463 lines
17 KiB
C#
463 lines
17 KiB
C#
using Microsoft.Extensions.Logging;
|
||
using Microsoft.Extensions.Options;
|
||
using OrpaonVision.Core.Results;
|
||
using OrpaonVision.Core.Security;
|
||
using OrpaonVision.Model.Configuration;
|
||
using OrpaonVision.Model.Security;
|
||
using OrpaonVision.SiteApp.Runtime.Contracts;
|
||
using OrpaonVision.SiteApp.Runtime.Options;
|
||
|
||
namespace OrpaonVision.SiteApp.Runtime.Services;
|
||
|
||
/// <summary>
|
||
/// 机种切换管理服务。
|
||
/// </summary>
|
||
public sealed class ProductSwitchManagerService : IProductSwitchManagerService
|
||
{
|
||
private readonly ILogger<ProductSwitchManagerService> _logger;
|
||
private readonly IProductPermissionService _permissionService;
|
||
private readonly IRuntimeStateMachineService _stateMachineService;
|
||
private readonly RuntimeOptions _runtimeOptions;
|
||
private readonly object _switchLock = new();
|
||
|
||
private Guid? _currentProductTypeId;
|
||
private ProductTypeModel? _currentProduct;
|
||
private DateTime _lastSwitchTime;
|
||
private readonly List<ProductSwitchRecordModel> _recentSwitches = new();
|
||
|
||
/// <summary>
|
||
/// 构造函数。
|
||
/// </summary>
|
||
public ProductSwitchManagerService(
|
||
ILogger<ProductSwitchManagerService> logger,
|
||
IProductPermissionService permissionService,
|
||
IRuntimeStateMachineService stateMachineService,
|
||
IOptions<RuntimeOptions> runtimeOptions)
|
||
{
|
||
_logger = logger;
|
||
_permissionService = permissionService;
|
||
_stateMachineService = stateMachineService;
|
||
_runtimeOptions = runtimeOptions.Value;
|
||
_lastSwitchTime = DateTime.UtcNow;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当前机种ID。
|
||
/// </summary>
|
||
public Guid? CurrentProductTypeId => _currentProductTypeId;
|
||
|
||
/// <summary>
|
||
/// 当前机种信息。
|
||
/// </summary>
|
||
public ProductTypeModel? CurrentProduct => _currentProduct;
|
||
|
||
/// <inheritdoc />
|
||
public async Task<Result<ProductSwitchResult>> SwitchProductAsync(Guid userId, string userName, string userRole, Guid targetProductTypeId, string switchReason, bool isForced = false, string? forcedReason = null, CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogInformation("开始机种切换: {UserId}, {UserName}, {TargetProductTypeId}, 强制: {IsForced}",
|
||
userId, userName, targetProductTypeId, isForced);
|
||
|
||
lock (_switchLock)
|
||
{
|
||
// 检查切换频率限制
|
||
var switchFrequencyResult = CheckSwitchFrequency();
|
||
if (!switchFrequencyResult.Succeeded)
|
||
{
|
||
return switchFrequencyResult;
|
||
}
|
||
|
||
// 检查运行时状态
|
||
var runtimeStateResult = CheckRuntimeState();
|
||
if (!runtimeStateResult.Succeeded)
|
||
{
|
||
return runtimeStateResult;
|
||
}
|
||
}
|
||
|
||
// 验证切换权限
|
||
var validationResult = await _permissionService.ValidateSwitchPermissionAsync(
|
||
userId, _currentProductTypeId, targetProductTypeId, isForced, cancellationToken);
|
||
|
||
if (!validationResult.Succeeded)
|
||
{
|
||
return Result<ProductSwitchResult>.Fail(validationResult.Code, validationResult.Message, validationResult.Errors.ToArray());
|
||
}
|
||
|
||
if (validationResult.Data == null || !validationResult.Data.CanSwitch)
|
||
{
|
||
return Result<ProductSwitchResult>.Fail("SWITCH_NOT_ALLOWED",
|
||
$"不允许切换机种: {string.Join(", ", validationResult.Data?.Errors ?? new List<string>())}");
|
||
}
|
||
|
||
// 执行切换
|
||
var switchResult = await ExecuteSwitchAsync(
|
||
userId, userName, userRole, targetProductTypeId, switchReason, isForced, forcedReason, validationResult.Data, cancellationToken);
|
||
|
||
return switchResult;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "机种切换失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "SWITCH_PRODUCT_FAILED", "机种切换失败。", traceId);
|
||
return Result<ProductSwitchResult>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<Result<ProductTypeModel>> GetCurrentProductAsync(CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
if (_currentProduct == null)
|
||
{
|
||
return Result<ProductTypeModel>.Fail("NO_CURRENT_PRODUCT", "当前没有选中的机种");
|
||
}
|
||
|
||
return Result<ProductTypeModel>.Success(_currentProduct);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "获取当前机种失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "GET_CURRENT_PRODUCT_FAILED", "获取当前机种失败。", traceId);
|
||
return Result<ProductTypeModel>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<Result<IReadOnlyList<ProductSwitchRecordModel>>> GetRecentSwitchesAsync(int count = 10, CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
var recentSwitches = _recentSwitches
|
||
.OrderByDescending(s => s.SwitchedAtUtc)
|
||
.Take(count)
|
||
.ToList();
|
||
|
||
return Result<IReadOnlyList<ProductSwitchRecordModel>>.Success(recentSwitches);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "获取最近切换记录失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "GET_RECENT_SWITCHES_FAILED", "获取最近切换记录失败。", traceId);
|
||
return Result<IReadOnlyList<ProductSwitchRecordModel>>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<Result<bool>> CanSwitchProductAsync(Guid userId, Guid targetProductTypeId, bool isForced = false, CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
// 检查基本条件
|
||
var frequencyResult = CheckSwitchFrequency();
|
||
if (!frequencyResult.Succeeded)
|
||
{
|
||
return Result<bool>.Success(false);
|
||
}
|
||
|
||
var runtimeStateResult = CheckRuntimeState();
|
||
if (!runtimeStateResult.Succeeded)
|
||
{
|
||
return Result<bool>.Success(false);
|
||
}
|
||
|
||
// 验证权限
|
||
var validationResult = await _permissionService.ValidateSwitchPermissionAsync(
|
||
userId, _currentProductTypeId, targetProductTypeId, isForced, cancellationToken);
|
||
|
||
return Result<bool>.Success(validationResult.Data?.CanSwitch ?? false);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "检查机种切换权限失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "CAN_SWITCH_FAILED", "检查机种切换权限失败。", traceId);
|
||
return Result<bool>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<Result> InitializeProductAsync(Guid productTypeId, CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogInformation("初始化机种: {ProductTypeId}", productTypeId);
|
||
|
||
// 获取机种信息
|
||
var product = await GetProductTypeAsync(productTypeId, cancellationToken);
|
||
if (product == null)
|
||
{
|
||
return Result.Fail("PRODUCT_NOT_FOUND", "机种不存在");
|
||
}
|
||
|
||
lock (_switchLock)
|
||
{
|
||
_currentProductTypeId = productTypeId;
|
||
_currentProduct = product;
|
||
_lastSwitchTime = DateTime.UtcNow;
|
||
}
|
||
|
||
_logger.LogInformation("机种初始化成功: {ProductCode}", product.Code);
|
||
return Result.Success();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "初始化机种失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "INITIALIZE_PRODUCT_FAILED", "初始化机种失败。", traceId);
|
||
return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
#region 私有方法
|
||
|
||
/// <summary>
|
||
/// 检查切换频率限制。
|
||
/// </summary>
|
||
private Result<ProductSwitchResult> CheckSwitchFrequency()
|
||
{
|
||
var minSwitchInterval = TimeSpan.FromMinutes(_runtimeOptions.MinSwitchIntervalMinutes);
|
||
var timeSinceLastSwitch = DateTime.UtcNow - _lastSwitchTime;
|
||
|
||
if (timeSinceLastSwitch < minSwitchInterval)
|
||
{
|
||
var remainingTime = minSwitchInterval - timeSinceLastSwitch;
|
||
return Result<ProductSwitchResult>.Fail("SWITCH_TOO_FREQUENT",
|
||
$"切换过于频繁,请等待 {remainingTime.TotalMinutes:F1} 分钟后再试");
|
||
}
|
||
|
||
return Result<ProductSwitchResult>.Success(new ProductSwitchResult());
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查运行时状态。
|
||
/// </summary>
|
||
private Result<ProductSwitchResult> CheckRuntimeState()
|
||
{
|
||
var snapshot = _stateMachineService.GetSnapshot();
|
||
|
||
// 如果正在运行中,不允许切换
|
||
if (string.Equals(snapshot.StateText, "Running", StringComparison.OrdinalIgnoreCase)
|
||
|| string.Equals(snapshot.StateText, "运行中", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return Result<ProductSwitchResult>.Fail("RUNTIME_RUNNING",
|
||
"运行时正在运行中,请先暂停或停止运行时再切换机种");
|
||
}
|
||
|
||
// 如果已完成,需要重置
|
||
if (snapshot.CurrentLayer > snapshot.TotalLayers)
|
||
{
|
||
return Result<ProductSwitchResult>.Fail("RUNTIME_COMPLETED",
|
||
"当前批次已完成,请重置运行时再切换机种");
|
||
}
|
||
|
||
return Result<ProductSwitchResult>.Success(new ProductSwitchResult());
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行机种切换。
|
||
/// </summary>
|
||
private async Task<Result<ProductSwitchResult>> ExecuteSwitchAsync(
|
||
Guid userId, string userName, string userRole, Guid targetProductTypeId,
|
||
string switchReason, bool isForced, string? forcedReason,
|
||
ProductSwitchValidationResult validationResult, CancellationToken cancellationToken)
|
||
{
|
||
// 获取目标机种信息
|
||
var targetProduct = await GetProductTypeAsync(targetProductTypeId, cancellationToken);
|
||
if (targetProduct == null)
|
||
{
|
||
return Result<ProductSwitchResult>.Fail("TARGET_PRODUCT_NOT_FOUND", "目标机种不存在");
|
||
}
|
||
|
||
var sourceProductTypeId = _currentProductTypeId;
|
||
var sourceProductCode = _currentProduct?.Code;
|
||
|
||
// 记录切换
|
||
var switchRecord = await _permissionService.RecordSwitchAsync(
|
||
userId, userName, userRole, sourceProductTypeId, targetProductTypeId,
|
||
isForced ? ProductSwitchType.Forced : ProductSwitchType.Normal,
|
||
switchReason, isForced, forcedReason, cancellationToken);
|
||
|
||
if (!switchRecord.Succeeded)
|
||
{
|
||
return Result<ProductSwitchResult>.Fail(switchRecord.Code, switchRecord.Message, switchRecord.Errors.ToArray());
|
||
}
|
||
|
||
// 更新当前机种
|
||
lock (_switchLock)
|
||
{
|
||
_currentProductTypeId = targetProductTypeId;
|
||
_currentProduct = targetProduct;
|
||
_lastSwitchTime = DateTime.UtcNow;
|
||
|
||
// 添加到最近切换记录
|
||
if (switchRecord.Data != null)
|
||
{
|
||
_recentSwitches.Add(switchRecord.Data);
|
||
}
|
||
|
||
// 保持最近100条记录
|
||
if (_recentSwitches.Count > 100)
|
||
{
|
||
_recentSwitches.RemoveAt(0);
|
||
}
|
||
}
|
||
|
||
// 重置运行时状态
|
||
_stateMachineService.Reset();
|
||
|
||
var result = new ProductSwitchResult
|
||
{
|
||
Success = true,
|
||
SourceProductCode = sourceProductCode,
|
||
TargetProductCode = targetProduct.Code,
|
||
SwitchType = isForced ? ProductSwitchType.Forced : ProductSwitchType.Normal,
|
||
SwitchedAtUtc = DateTime.UtcNow,
|
||
SwitchRecordId = switchRecord.Data?.Id ?? Guid.Empty,
|
||
Warnings = validationResult.Warnings
|
||
};
|
||
|
||
_logger.LogInformation("机种切换成功: {SourceProductCode} -> {TargetProductCode}",
|
||
sourceProductCode, targetProduct.Code);
|
||
|
||
return Result<ProductSwitchResult>.Success(result);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取机种信息。
|
||
/// </summary>
|
||
private async Task<ProductTypeModel?> GetProductTypeAsync(Guid productTypeId, CancellationToken cancellationToken = default)
|
||
{
|
||
// 模拟实现,实际应该从数据库或缓存获取
|
||
await Task.Delay(10, cancellationToken);
|
||
|
||
return new ProductTypeModel
|
||
{
|
||
Id = productTypeId,
|
||
Code = productTypeId.ToString()[..8], // 取前8位作为编码
|
||
Name = $"机种 {productTypeId.ToString()[..8]}",
|
||
Status = ProductTypeStatus.Published,
|
||
TotalLayers = 3,
|
||
IsPublished = true,
|
||
CreatedAtUtc = DateTime.UtcNow.AddDays(-30),
|
||
CreatedBy = "System"
|
||
};
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
|
||
/// <summary>
|
||
/// 机种切换管理服务接口。
|
||
/// </summary>
|
||
public interface IProductSwitchManagerService
|
||
{
|
||
/// <summary>
|
||
/// 当前机种ID。
|
||
/// </summary>
|
||
Guid? CurrentProductTypeId { get; }
|
||
|
||
/// <summary>
|
||
/// 当前机种信息。
|
||
/// </summary>
|
||
ProductTypeModel? CurrentProduct { get; }
|
||
|
||
/// <summary>
|
||
/// 切换机种。
|
||
/// </summary>
|
||
/// <param name="userId">用户ID。</param>
|
||
/// <param name="userName">用户姓名。</param>
|
||
/// <param name="userRole">用户角色。</param>
|
||
/// <param name="targetProductTypeId">目标机种ID。</param>
|
||
/// <param name="switchReason">切换原因。</param>
|
||
/// <param name="isForced">是否强制切换。</param>
|
||
/// <param name="forcedReason">强制切换原因。</param>
|
||
/// <param name="cancellationToken">取消令牌。</param>
|
||
/// <returns>切换结果。</returns>
|
||
Task<Result<ProductSwitchResult>> SwitchProductAsync(Guid userId, string userName, string userRole, Guid targetProductTypeId, string switchReason, bool isForced = false, string? forcedReason = null, CancellationToken cancellationToken = default);
|
||
|
||
/// <summary>
|
||
/// 获取当前机种。
|
||
/// </summary>
|
||
/// <param name="cancellationToken">取消令牌。</param>
|
||
/// <returns>当前机种信息。</returns>
|
||
Task<Result<ProductTypeModel>> GetCurrentProductAsync(CancellationToken cancellationToken = default);
|
||
|
||
/// <summary>
|
||
/// 获取最近切换记录。
|
||
/// </summary>
|
||
/// <param name="count">记录数量。</param>
|
||
/// <param name="cancellationToken">取消令牌。</param>
|
||
/// <returns>切换记录列表。</returns>
|
||
Task<Result<IReadOnlyList<ProductSwitchRecordModel>>> GetRecentSwitchesAsync(int count = 10, CancellationToken cancellationToken = default);
|
||
|
||
/// <summary>
|
||
/// 检查是否可以切换机种。
|
||
/// </summary>
|
||
/// <param name="userId">用户ID。</param>
|
||
/// <param name="targetProductTypeId">目标机种ID。</param>
|
||
/// <param name="isForced">是否强制切换。</param>
|
||
/// <param name="cancellationToken">取消令牌。</param>
|
||
/// <returns>是否可以切换。</returns>
|
||
Task<Result<bool>> CanSwitchProductAsync(Guid userId, Guid targetProductTypeId, bool isForced = false, CancellationToken cancellationToken = default);
|
||
|
||
/// <summary>
|
||
/// 初始化机种。
|
||
/// </summary>
|
||
/// <param name="productTypeId">机种ID。</param>
|
||
/// <param name="cancellationToken">取消令牌。</param>
|
||
/// <returns>初始化结果。</returns>
|
||
Task<Result> InitializeProductAsync(Guid productTypeId, CancellationToken cancellationToken = default);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 机种切换结果。
|
||
/// </summary>
|
||
public sealed class ProductSwitchResult
|
||
{
|
||
/// <summary>
|
||
/// 是否成功。
|
||
/// </summary>
|
||
public bool Success { get; init; }
|
||
|
||
/// <summary>
|
||
/// 源机种编码。
|
||
/// </summary>
|
||
public string? SourceProductCode { get; init; }
|
||
|
||
/// <summary>
|
||
/// 目标机种编码。
|
||
/// </summary>
|
||
public string? TargetProductCode { get; init; }
|
||
|
||
/// <summary>
|
||
/// 切换类型。
|
||
/// </summary>
|
||
public ProductSwitchType SwitchType { get; init; }
|
||
|
||
/// <summary>
|
||
/// 切换时间(UTC)。
|
||
/// </summary>
|
||
public DateTime SwitchedAtUtc { get; init; }
|
||
|
||
/// <summary>
|
||
/// 切换记录ID。
|
||
/// </summary>
|
||
public Guid SwitchRecordId { get; init; }
|
||
|
||
/// <summary>
|
||
/// 警告信息。
|
||
/// </summary>
|
||
public List<string> Warnings { get; init; } = new();
|
||
|
||
/// <summary>
|
||
/// 错误信息。
|
||
/// </summary>
|
||
public List<string> Errors { get; init; } = new();
|
||
}
|