diff --git a/OrpaonVision.Core/Production/IRunLayerSessionRepository.cs b/OrpaonVision.Core/Production/IRunLayerSessionRepository.cs
new file mode 100644
index 0000000..785b605
--- /dev/null
+++ b/OrpaonVision.Core/Production/IRunLayerSessionRepository.cs
@@ -0,0 +1,86 @@
+using OrpaonVision.Core.Results;
+using OrpaonVision.Model.Production;
+
+namespace OrpaonVision.Core.Production;
+
+///
+/// 运行层级会话仓储接口。
+///
+public interface IRunLayerSessionRepository
+{
+ ///
+ /// 创建层级会话。
+ ///
+ /// 层级会话模型。
+ /// 取消令牌。
+ /// 创建结果。
+ Task> CreateAsync(RunLayerSessionModel layerSession, CancellationToken cancellationToken = default);
+
+ ///
+ /// 更新层级会话。
+ ///
+ /// 层级会话模型。
+ /// 取消令牌。
+ /// 更新结果。
+ Task> UpdateAsync(RunLayerSessionModel layerSession, CancellationToken cancellationToken = default);
+
+ ///
+ /// 根据ID获取层级会话。
+ ///
+ /// 层级会话ID。
+ /// 取消令牌。
+ /// 层级会话模型。
+ Task> GetByIdAsync(Guid layerSessionId, CancellationToken cancellationToken = default);
+
+ ///
+ /// 根据产品会话ID获取层级会话列表。
+ ///
+ /// 产品会话ID。
+ /// 取消令牌。
+ /// 层级会话列表。
+ Task>> GetBySessionIdAsync(Guid sessionId, CancellationToken cancellationToken = default);
+
+ ///
+ /// 根据状态获取层级会话列表。
+ ///
+ /// 状态。
+ /// 取消令牌。
+ /// 层级会话列表。
+ Task>> GetByStatusAsync(RunLayerSessionStatus status, CancellationToken cancellationToken = default);
+
+ ///
+ /// 分页查询层级会话。
+ ///
+ /// 页索引。
+ /// 页大小。
+ /// 产品类型编码(可选)。
+ /// 状态(可选)。
+ /// 开始时间(可选)。
+ /// 结束时间(可选)。
+ /// 取消令牌。
+ /// 分页结果。
+ Task>> GetPagedAsync(
+ int pageIndex,
+ int pageSize,
+ string? productTypeCode = null,
+ RunLayerSessionStatus? status = null,
+ DateTime? startTime = null,
+ DateTime? endTime = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// 删除层级会话。
+ ///
+ /// 层级会话ID。
+ /// 取消令牌。
+ /// 删除结果。
+ Task> DeleteAsync(Guid layerSessionId, CancellationToken cancellationToken = default);
+
+ ///
+ /// 批量删除层级会话。
+ ///
+ /// 产品会话ID列表。
+ /// 取消令牌。
+ /// 删除结果。
+ Task> DeleteBySessionIdsAsync(List sessionIds, CancellationToken cancellationToken = default);
+}
diff --git a/OrpaonVision.Core/Production/IRunPartJudgmentRepository.cs b/OrpaonVision.Core/Production/IRunPartJudgmentRepository.cs
new file mode 100644
index 0000000..7cc96a6
--- /dev/null
+++ b/OrpaonVision.Core/Production/IRunPartJudgmentRepository.cs
@@ -0,0 +1,102 @@
+using OrpaonVision.Core.Results;
+using OrpaonVision.Model.Production;
+
+namespace OrpaonVision.Core.Production;
+
+///
+/// 运行部件判定仓储接口。
+///
+public interface IRunPartJudgmentRepository
+{
+ ///
+ /// 创建部件判定。
+ ///
+ /// 部件判定模型。
+ /// 取消令牌。
+ /// 创建结果。
+ Task> CreateAsync(RunPartJudgmentModel judgment, CancellationToken cancellationToken = default);
+
+ ///
+ /// 更新部件判定。
+ ///
+ /// 部件判定模型。
+ /// 取消令牌。
+ /// 更新结果。
+ Task> UpdateAsync(RunPartJudgmentModel judgment, CancellationToken cancellationToken = default);
+
+ ///
+ /// 根据ID获取部件判定。
+ ///
+ /// 判定ID。
+ /// 取消令牌。
+ /// 部件判定模型。
+ Task> GetByIdAsync(Guid judgmentId, CancellationToken cancellationToken = default);
+
+ ///
+ /// 根据层级会话ID获取部件判定列表。
+ ///
+ /// 层级会话ID。
+ /// 取消令牌。
+ /// 部件判定列表。
+ Task>> GetByLayerSessionIdAsync(Guid layerSessionId, CancellationToken cancellationToken = default);
+
+ ///
+ /// 根据产品会话ID获取部件判定列表。
+ ///
+ /// 产品会话ID。
+ /// 取消令牌。
+ /// 部件判定列表。
+ Task>> GetBySessionIdAsync(Guid sessionId, CancellationToken cancellationToken = default);
+
+ ///
+ /// 根据结果获取部件判定列表。
+ ///
+ /// 判定结果。
+ /// 取消令牌。
+ /// 部件判定列表。
+ Task>> GetByResultAsync(RunPartJudgmentResult result, CancellationToken cancellationToken = default);
+
+ ///
+ /// 分页查询部件判定。
+ ///
+ /// 页索引。
+ /// 页大小。
+ /// 产品类型编码(可选)。
+ /// 判定结果(可选)。
+ /// 开始时间(可选)。
+ /// 结束时间(可选)。
+ /// 取消令牌。
+ /// 分页结果。
+ Task>> GetPagedAsync(
+ int pageIndex,
+ int pageSize,
+ string? productTypeCode = null,
+ RunPartJudgmentResult? result = null,
+ DateTime? startTime = null,
+ DateTime? endTime = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// 删除部件判定。
+ ///
+ /// 判定ID。
+ /// 取消令牌。
+ /// 删除结果。
+ Task> DeleteAsync(Guid judgmentId, CancellationToken cancellationToken = default);
+
+ ///
+ /// 批量删除部件判定。
+ ///
+ /// 产品会话ID列表。
+ /// 取消令牌。
+ /// 删除结果。
+ Task> DeleteBySessionIdsAsync(List sessionIds, CancellationToken cancellationToken = default);
+
+ ///
+ /// 批量删除层级会话的部件判定。
+ ///
+ /// 层级会话ID列表。
+ /// 取消令牌。
+ /// 删除结果。
+ Task> DeleteByLayerSessionIdsAsync(List layerSessionIds, CancellationToken cancellationToken = default);
+}
diff --git a/OrpaonVision.Core/Production/IRunStateTransitionLogRepository.cs b/OrpaonVision.Core/Production/IRunStateTransitionLogRepository.cs
new file mode 100644
index 0000000..6cc28da
--- /dev/null
+++ b/OrpaonVision.Core/Production/IRunStateTransitionLogRepository.cs
@@ -0,0 +1,104 @@
+using OrpaonVision.Core.Results;
+using OrpaonVision.Model.Production;
+
+namespace OrpaonVision.Core.Production;
+
+///
+/// 运行状态转换日志仓储接口。
+///
+public interface IRunStateTransitionLogRepository
+{
+ ///
+ /// 创建状态转换日志。
+ ///
+ /// 状态转换日志模型。
+ /// 取消令牌。
+ /// 创建结果。
+ Task> CreateAsync(RunStateTransitionLogModel log, CancellationToken cancellationToken = default);
+
+ ///
+ /// 根据ID获取状态转换日志。
+ ///
+ /// 日志ID。
+ /// 取消令牌。
+ /// 状态转换日志模型。
+ Task> GetByIdAsync(Guid logId, CancellationToken cancellationToken = default);
+
+ ///
+ /// 根据产品会话ID获取状态转换日志列表。
+ ///
+ /// 产品会话ID。
+ /// 取消令牌。
+ /// 状态转换日志列表。
+ Task>> GetBySessionIdAsync(Guid sessionId, CancellationToken cancellationToken = default);
+
+ ///
+ /// 根据层级会话ID获取状态转换日志列表。
+ ///
+ /// 层级会话ID。
+ /// 取消令牌。
+ /// 状态转换日志列表。
+ Task>> GetByLayerSessionIdAsync(Guid layerSessionId, CancellationToken cancellationToken = default);
+
+ ///
+ /// 根据转换类型获取状态转换日志列表。
+ ///
+ /// 转换类型。
+ /// 取消令牌。
+ /// 状态转换日志列表。
+ Task>> GetByTransitionTypeAsync(RunStateTransitionType transitionType, CancellationToken cancellationToken = default);
+
+ ///
+ /// 根据人工干预标志获取状态转换日志列表。
+ ///
+ /// 是否人工干预。
+ /// 取消令牌。
+ /// 状态转换日志列表。
+ Task>> GetByManualInterventionAsync(bool isManualIntervention, CancellationToken cancellationToken = default);
+
+ ///
+ /// 分页查询状态转换日志。
+ ///
+ /// 页索引。
+ /// 页大小。
+ /// 产品类型编码(可选)。
+ /// 转换类型(可选)。
+ /// 是否人工干预(可选)。
+ /// 开始时间(可选)。
+ /// 结束时间(可选)。
+ /// 取消令牌。
+ /// 分页结果。
+ Task>> GetPagedAsync(
+ int pageIndex,
+ int pageSize,
+ string? productTypeCode = null,
+ RunStateTransitionType? transitionType = null,
+ bool? isManualIntervention = null,
+ DateTime? startTime = null,
+ DateTime? endTime = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// 删除状态转换日志。
+ ///
+ /// 日志ID。
+ /// 取消令牌。
+ /// 删除结果。
+ Task> DeleteAsync(Guid logId, CancellationToken cancellationToken = default);
+
+ ///
+ /// 批量删除状态转换日志。
+ ///
+ /// 产品会话ID列表。
+ /// 取消令牌。
+ /// 删除结果。
+ Task> DeleteBySessionIdsAsync(List sessionIds, CancellationToken cancellationToken = default);
+
+ ///
+ /// 清理指定日期之前的日志。
+ ///
+ /// 指定日期。
+ /// 取消令牌。
+ /// 删除的记录数。
+ Task> CleanupLogsBeforeAsync(DateTime beforeDate, CancellationToken cancellationToken = default);
+}
diff --git a/OrpaonVision.Model/Production/RunLayerSessionModel.cs b/OrpaonVision.Model/Production/RunLayerSessionModel.cs
new file mode 100644
index 0000000..6713f16
--- /dev/null
+++ b/OrpaonVision.Model/Production/RunLayerSessionModel.cs
@@ -0,0 +1,174 @@
+namespace OrpaonVision.Model.Production;
+
+///
+/// 运行层级会话模型。
+///
+public sealed class RunLayerSessionModel
+{
+ ///
+ /// 层级会话ID。
+ ///
+ public Guid LayerSessionId { get; set; }
+
+ ///
+ /// 产品会话ID。
+ ///
+ public Guid SessionId { get; set; }
+
+ ///
+ /// 层级序号。
+ ///
+ public int LayerNumber { get; set; }
+
+ ///
+ /// 层级名称。
+ ///
+ public string LayerName { get; set; } = string.Empty;
+
+ ///
+ /// 层级编码。
+ ///
+ public string LayerCode { get; set; } = string.Empty;
+
+ ///
+ /// 开始时间(UTC)。
+ ///
+ public DateTime StartedAtUtc { get; set; }
+
+ ///
+ /// 结束时间(UTC)。
+ ///
+ public DateTime? EndedAtUtc { get; set; }
+
+ ///
+ /// 层级状态。
+ ///
+ public RunLayerSessionStatus Status { get; set; }
+
+ ///
+ /// 检测到的部件数量。
+ ///
+ public int DetectedPartsCount { get; set; }
+
+ ///
+ /// 合格部件数量。
+ ///
+ public int QualifiedPartsCount { get; set; }
+
+ ///
+ /// NG部件数量。
+ ///
+ public int NgPartsCount { get; set; }
+
+ ///
+ /// 处理的图像数量。
+ ///
+ public int ProcessedImageCount { get; set; }
+
+ ///
+ /// 首张截图路径。
+ ///
+ public string? FirstScreenshotPath { get; set; }
+
+ ///
+ /// 末张截图路径。
+ ///
+ public string? LastScreenshotPath { get; set; }
+
+ ///
+ /// 层级结果。
+ ///
+ public RunLayerSessionResult Result { get; set; }
+
+ ///
+ /// NG原因描述。
+ ///
+ public string? NgReason { get; set; }
+
+ ///
+ /// 处理耗时(毫秒)。
+ ///
+ public long ProcessingTimeMs { get; set; }
+
+ ///
+ /// 创建时间(UTC)。
+ ///
+ public DateTime CreatedAtUtc { get; set; }
+
+ ///
+ /// 更新时间(UTC)。
+ ///
+ public DateTime UpdatedAtUtc { get; set; }
+
+ ///
+ /// 创建人。
+ ///
+ public string CreatedBy { get; set; } = string.Empty;
+
+ ///
+ /// 更新人。
+ ///
+ public string UpdatedBy { get; set; } = string.Empty;
+}
+
+///
+/// 运行层级会话状态。
+///
+public enum RunLayerSessionStatus
+{
+ ///
+ /// 未开始。
+ ///
+ NotStarted = 0,
+
+ ///
+ /// 进行中。
+ ///
+ InProgress = 1,
+
+ ///
+ /// 已完成(OK)。
+ ///
+ CompletedOk = 2,
+
+ ///
+ /// 已完成(NG)。
+ ///
+ CompletedNg = 3,
+
+ ///
+ /// 已跳过。
+ ///
+ Skipped = 4,
+
+ ///
+ /// 异常终止。
+ ///
+ Error = 5
+}
+
+///
+/// 运行层级会话结果。
+///
+public enum RunLayerSessionResult
+{
+ ///
+ /// 待定。
+ ///
+ Pending = 0,
+
+ ///
+ /// 合格。
+ ///
+ Ok = 1,
+
+ ///
+ /// 不合格。
+ ///
+ Ng = 2,
+
+ ///
+ /// 跳过。
+ ///
+ Skipped = 3
+}
diff --git a/OrpaonVision.Model/Production/RunPartJudgmentModel.cs b/OrpaonVision.Model/Production/RunPartJudgmentModel.cs
new file mode 100644
index 0000000..0f87e88
--- /dev/null
+++ b/OrpaonVision.Model/Production/RunPartJudgmentModel.cs
@@ -0,0 +1,229 @@
+namespace OrpaonVision.Model.Production;
+
+///
+/// 运行部件判定模型。
+///
+public sealed class RunPartJudgmentModel
+{
+ ///
+ /// 判定ID。
+ ///
+ public Guid JudgmentId { get; set; }
+
+ ///
+ /// 产品会话ID。
+ ///
+ public Guid SessionId { get; set; }
+
+ ///
+ /// 层级会话ID。
+ ///
+ public Guid LayerSessionId { get; set; }
+
+ ///
+ /// 部件序号(在层级中的序号)。
+ ///
+ public int PartSequence { get; set; }
+
+ ///
+ /// 部件编码。
+ ///
+ public string PartCode { get; set; } = string.Empty;
+
+ ///
+ /// 部件名称。
+ ///
+ public string PartName { get; set; } = string.Empty;
+
+ ///
+ /// 检测到的类别。
+ ///
+ public string DetectedClass { get; set; } = string.Empty;
+
+ ///
+ /// 置信度。
+ ///
+ public decimal Confidence { get; set; }
+
+ ///
+ /// 边界框X坐标。
+ ///
+ public float BoundingBoxX { get; set; }
+
+ ///
+ /// 边界框Y坐标。
+ ///
+ public float BoundingBoxY { get; set; }
+
+ ///
+ /// 边界框宽度。
+ ///
+ public float BoundingBoxWidth { get; set; }
+
+ ///
+ /// 边界框高度。
+ ///
+ public float BoundingBoxHeight { get; set; }
+
+ ///
+ /// 实际中心点X坐标。
+ ///
+ public float ActualCenterX { get; set; }
+
+ ///
+ /// 实际中心点Y坐标。
+ ///
+ public float ActualCenterY { get; set; }
+
+ ///
+ /// 期望中心点X坐标。
+ ///
+ public float ExpectedCenterX { get; set; }
+
+ ///
+ /// 期望中心点Y坐标。
+ ///
+ public float ExpectedCenterY { get; set; }
+
+ ///
+ /// 位置偏差(像素)。
+ ///
+ public float PositionDeviation { get; set; }
+
+ ///
+ /// 判定状态。
+ ///
+ public RunPartJudgmentStatus Status { get; set; }
+
+ ///
+ /// 判定结果。
+ ///
+ public RunPartJudgmentResult Result { get; set; }
+
+ ///
+ /// 判定规则ID。
+ ///
+ public Guid? RuleId { get; set; }
+
+ ///
+ /// 判定规则名称。
+ ///
+ public string? RuleName { get; set; }
+
+ ///
+ /// NG原因。
+ ///
+ public string? NgReason { get; set; }
+
+ ///
+ /// 截图路径。
+ ///
+ public string? ScreenshotPath { get; set; }
+
+ ///
+ /// 判定时间(UTC)。
+ ///
+ public DateTime JudgedAtUtc { get; set; }
+
+ ///
+ /// 推理耗时(毫秒)。
+ ///
+ public long InferenceTimeMs { get; set; }
+
+ ///
+ /// 创建时间(UTC)。
+ ///
+ public DateTime CreatedAtUtc { get; set; }
+
+ ///
+ /// 更新时间(UTC)。
+ ///
+ public DateTime UpdatedAtUtc { get; set; }
+
+ ///
+ /// 创建人。
+ ///
+ public string CreatedBy { get; set; } = string.Empty;
+
+ ///
+ /// 更新人。
+ ///
+ public string UpdatedBy { get; set; } = string.Empty;
+}
+
+///
+/// 运行部件判定状态。
+///
+public enum RunPartJudgmentStatus
+{
+ ///
+ /// 待检测。
+ ///
+ Pending = 0,
+
+ ///
+ /// 检测中。
+ ///
+ Detecting = 1,
+
+ ///
+ /// 已检测。
+ ///
+ Detected = 2,
+
+ ///
+ /// 判定中。
+ ///
+ Judging = 3,
+
+ ///
+ /// 已判定。
+ ///
+ Judged = 4,
+
+ ///
+ /// 异常。
+ ///
+ Error = 5
+}
+
+///
+/// 违行部件判定结果。
+///
+public enum RunPartJudgmentResult
+{
+ ///
+ /// 待定。
+ ///
+ Pending = 0,
+
+ ///
+ /// 合格。
+ ///
+ Ok = 1,
+
+ ///
+ /// 不合格。
+ ///
+ Ng = 2,
+
+ ///
+ /// 缺失。
+ ///
+ Missing = 3,
+
+ ///
+ /// 位置错误。
+ ///
+ PositionError = 4,
+
+ ///
+ /// 禁装。
+ ///
+ Forbidden = 5,
+
+ ///
+ /// 重复。
+ ///
+ Duplicate = 6
+}
diff --git a/OrpaonVision.Model/Production/RunStateTransitionLogModel.cs b/OrpaonVision.Model/Production/RunStateTransitionLogModel.cs
new file mode 100644
index 0000000..62a8d3a
--- /dev/null
+++ b/OrpaonVision.Model/Production/RunStateTransitionLogModel.cs
@@ -0,0 +1,133 @@
+namespace OrpaonVision.Model.Production;
+
+///
+/// 运行状态转换日志模型。
+///
+public sealed class RunStateTransitionLogModel
+{
+ ///
+ /// 日志ID。
+ ///
+ public Guid LogId { get; set; }
+
+ ///
+ /// 产品会话ID。
+ ///
+ public Guid SessionId { get; set; }
+
+ ///
+ /// 层级会话ID(可选)。
+ ///
+ public Guid? LayerSessionId { get; set; }
+
+ ///
+ /// 转换前状态。
+ ///
+ public string FromState { get; set; } = string.Empty;
+
+ ///
+ /// 转换后状态。
+ ///
+ public string ToState { get; set; } = string.Empty;
+
+ ///
+ /// 触发事件。
+ ///
+ public string TriggerEvent { get; set; } = string.Empty;
+
+ ///
+ /// 转换原因。
+ ///
+ public string Reason { get; set; } = string.Empty;
+
+ ///
+ /// 转换类型。
+ ///
+ public RunStateTransitionType TransitionType { get; set; }
+
+ ///
+ /// 操作者ID。
+ ///
+ public string? OperatorId { get; set; }
+
+ ///
+ /// 操作者姓名。
+ ///
+ public string? OperatorName { get; set; }
+
+ ///
+ /// 是否人工干预。
+ ///
+ public bool IsManualIntervention { get; set; }
+
+ ///
+ /// 关联数据(JSON格式)。
+ ///
+ public string? ContextJson { get; set; }
+
+ ///
+ /// 截图路径(可选)。
+ ///
+ public string? ScreenshotPath { get; set; }
+
+ ///
+ /// 转换时间(UTC)。
+ ///
+ public DateTime TransitionAtUtc { get; set; }
+
+ ///
+ /// 处理耗时(毫秒)。
+ ///
+ public long ProcessingTimeMs { get; set; }
+
+ ///
+ /// 错误信息(如果有)。
+ ///
+ public string? ErrorMessage { get; set; }
+
+ ///
+ /// 创建时间(UTC)。
+ ///
+ public DateTime CreatedAtUtc { get; set; }
+
+ ///
+ /// 创建人。
+ ///
+ public string CreatedBy { get; set; } = string.Empty;
+}
+
+///
+/// 运行状态转换类型。
+///
+public enum RunStateTransitionType
+{
+ ///
+ /// 自动转换。
+ ///
+ Automatic = 0,
+
+ ///
+ /// 人工干预。
+ ///
+ Manual = 1,
+
+ ///
+ /// 系统触发。
+ ///
+ System = 2,
+
+ ///
+ /// 错误恢复。
+ ///
+ ErrorRecovery = 3,
+
+ ///
+ /// 超时处理。
+ ///
+ Timeout = 4,
+
+ ///
+ /// 异常终止。
+ ///
+ Exception = 5
+}
diff --git a/OrpaonVision.SiteApp.Tests/OrpaonVision.SiteApp.Tests.csproj b/OrpaonVision.SiteApp.Tests/OrpaonVision.SiteApp.Tests.csproj
new file mode 100644
index 0000000..ca8dbf8
--- /dev/null
+++ b/OrpaonVision.SiteApp.Tests/OrpaonVision.SiteApp.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net8.0-windows
+ enable
+ enable
+ false
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OrpaonVision.SiteApp.Tests/Runtime/Services/RuntimeStateMachineServiceTests.cs b/OrpaonVision.SiteApp.Tests/Runtime/Services/RuntimeStateMachineServiceTests.cs
new file mode 100644
index 0000000..78f399d
--- /dev/null
+++ b/OrpaonVision.SiteApp.Tests/Runtime/Services/RuntimeStateMachineServiceTests.cs
@@ -0,0 +1,128 @@
+using Microsoft.Extensions.Logging;
+using Moq;
+using OrpaonVision.Core.Results;
+using OrpaonVision.SiteApp.Runtime.Contracts;
+using OrpaonVision.SiteApp.Runtime.Services;
+
+namespace OrpaonVision.SiteApp.Tests.Runtime.Services;
+
+///
+/// 运行时状态机服务单元测试。
+///
+[TestClass]
+public class RuntimeStateMachineServiceTests
+{
+ private IRuntimeStateMachineService _stateMachine = null!;
+ private Mock> _loggerMock = null!;
+
+ [TestInitialize]
+ public void TestInitialize()
+ {
+ _loggerMock = new Mock>();
+
+ // 使用简单状态机实现进行测试
+ _stateMachine = new SimpleRuntimeStateMachineService(_loggerMock.Object);
+ }
+
+ [TestMethod]
+ public void GetSnapshot_InitialState_ReturnsCorrectSnapshot()
+ {
+ // Act
+ var snapshot = _stateMachine.GetSnapshot();
+
+ // Assert
+ Assert.IsNotNull(snapshot);
+ Assert.AreEqual(0, snapshot.CurrentLayer);
+ Assert.AreEqual(0, snapshot.TotalLayers);
+ Assert.IsTrue(snapshot.StateText.Contains("Idle") || snapshot.StateText.Contains("初始"));
+ }
+
+ [TestMethod]
+ public void MoveToNextLayer_FromInitialState_ReturnsSuccess()
+ {
+ // Act
+ var result = _stateMachine.MoveToNextLayer();
+
+ // Assert
+ Assert.IsTrue(result.Succeeded);
+ var snapshot = _stateMachine.GetSnapshot();
+ Assert.AreEqual(1, snapshot.CurrentLayer);
+ }
+
+ [TestMethod]
+ public void MoveToNextLayer_SequentialCalls_ReturnsSuccess()
+ {
+ // Act & Assert - 第一层
+ var result1 = _stateMachine.MoveToNextLayer();
+ Assert.IsTrue(result1.Succeeded);
+ var snapshot1 = _stateMachine.GetSnapshot();
+ Assert.AreEqual(1, snapshot1.CurrentLayer);
+
+ // Act & Assert - 第二层
+ var result2 = _stateMachine.MoveToNextLayer();
+ Assert.IsTrue(result2.Succeeded);
+ var snapshot2 = _stateMachine.GetSnapshot();
+ Assert.AreEqual(2, snapshot2.CurrentLayer);
+ }
+
+ [TestMethod]
+ public void Reset_AfterProgress_ReturnsToInitialState()
+ {
+ // Arrange - 推进几层
+ _stateMachine.MoveToNextLayer();
+ _stateMachine.MoveToNextLayer();
+ var snapshotBefore = _stateMachine.GetSnapshot();
+ Assert.AreEqual(2, snapshotBefore.CurrentLayer);
+
+ // Act
+ _stateMachine.Reset();
+
+ // Assert
+ var snapshotAfter = _stateMachine.GetSnapshot();
+ Assert.AreEqual(0, snapshotAfter.CurrentLayer);
+ Assert.AreEqual(0, snapshotAfter.TotalLayers);
+ }
+
+ [TestMethod]
+ public void StateTransitionFlow_CompleteWorkflow_ReturnsExpectedStates()
+ {
+ // Arrange & Act - 初始状态
+ var initialSnapshot = _stateMachine.GetSnapshot();
+ Assert.AreEqual(0, initialSnapshot.CurrentLayer);
+
+ // Act & Assert - 第一层
+ var result1 = _stateMachine.MoveToNextLayer();
+ Assert.IsTrue(result1.Succeeded);
+ var snapshot1 = _stateMachine.GetSnapshot();
+ Assert.AreEqual(1, snapshot1.CurrentLayer);
+
+ // Act & Assert - 第二层
+ var result2 = _stateMachine.MoveToNextLayer();
+ Assert.IsTrue(result2.Succeeded);
+ var snapshot2 = _stateMachine.GetSnapshot();
+ Assert.AreEqual(2, snapshot2.CurrentLayer);
+
+ // Act & Assert - 重置
+ _stateMachine.Reset();
+ var finalSnapshot = _stateMachine.GetSnapshot();
+ Assert.AreEqual(0, finalSnapshot.CurrentLayer);
+ }
+
+ [TestMethod]
+ public void GetSnapshot_AfterMultipleTransitions_ReturnsCorrectCurrentState()
+ {
+ // Arrange
+ for (int i = 0; i < 5; i++)
+ {
+ _stateMachine.MoveToNextLayer();
+ }
+
+ // Act
+ var snapshot = _stateMachine.GetSnapshot();
+
+ // Assert
+ Assert.AreEqual(5, snapshot.CurrentLayer);
+ Assert.IsNotNull(snapshot.StateText);
+ Assert.IsFalse(string.IsNullOrEmpty(snapshot.StateText));
+ }
+}
diff --git a/OrpaonVision.SiteApp.Tests/Training/ModelPackageImportActivationTests.cs b/OrpaonVision.SiteApp.Tests/Training/ModelPackageImportActivationTests.cs
new file mode 100644
index 0000000..255a062
--- /dev/null
+++ b/OrpaonVision.SiteApp.Tests/Training/ModelPackageImportActivationTests.cs
@@ -0,0 +1,238 @@
+using Microsoft.Extensions.Logging;
+using Moq;
+using OrpaonVision.Core.Results;
+using OrpaonVision.Core.Training;
+using OrpaonVision.Core.Training.Contracts;
+using OrpaonVision.Core.Training.Contracts.Commands;
+
+namespace OrpaonVision.SiteApp.Tests.Training;
+
+///
+/// 模型包导入激活单元测试。
+///
+[TestClass]
+public class ModelPackageImportActivationTests
+{
+ private Mock _modelPackageServiceMock = null!;
+ private Mock> _loggerMock = null!;
+
+ [TestInitialize]
+ public void TestInitialize()
+ {
+ _modelPackageServiceMock = new Mock();
+ _loggerMock = new Mock>();
+ }
+
+ [TestMethod]
+ public async Task ImportPackageAsync_ValidPackage_ReturnsSuccess()
+ {
+ // Arrange
+ var packagePath = "test_package.ovpkg";
+ var expectedResult = new ModelPackageImportResultDto
+ {
+ ModelPackageId = Guid.NewGuid(),
+ Status = ModelPackageImportStatus.Imported,
+ Message = "导入成功",
+ ImportedAtUtc = DateTime.UtcNow,
+ ImportedBy = "TestUser"
+ };
+
+ _modelPackageServiceMock
+ .Setup(x => x.ImportPackageAsync(packagePath, It.IsAny()))
+ .ReturnsAsync(Result.Success(expectedResult));
+
+ // Act
+ var result = await _modelPackageServiceMock.Object.ImportPackageAsync(packagePath);
+
+ // Assert
+ Assert.IsTrue(result.Succeeded);
+ Assert.IsNotNull(result.Data);
+ Assert.AreEqual(ModelPackageImportStatus.Imported, result.Data.Status);
+ Assert.AreEqual("导入成功", result.Data.Message);
+ Assert.AreEqual("TestUser", result.Data.ImportedBy);
+ }
+
+ [TestMethod]
+ public async Task ImportPackageAsync_InvalidPackage_ReturnsFailure()
+ {
+ // Arrange
+ var packagePath = "invalid_package.ovpkg";
+ var expectedError = Result.Fail("INVALID_PACKAGE", "模型包格式无效");
+
+ _modelPackageServiceMock
+ .Setup(x => x.ImportPackageAsync(packagePath, It.IsAny()))
+ .ReturnsAsync(expectedError);
+
+ // Act
+ var result = await _modelPackageServiceMock.Object.ImportPackageAsync(packagePath);
+
+ // Assert
+ Assert.IsFalse(result.Succeeded);
+ Assert.AreEqual("INVALID_PACKAGE", result.Code);
+ Assert.AreEqual("模型包格式无效", result.Message);
+ }
+
+ [TestMethod]
+ public async Task ImportPackageAsync_MissingFile_ReturnsFailure()
+ {
+ // Arrange
+ var packagePath = "missing_package.ovpkg";
+ var expectedError = Result.Fail("FILE_NOT_FOUND", "模型包文件不存在");
+
+ _modelPackageServiceMock
+ .Setup(x => x.ImportPackageAsync(packagePath, It.IsAny()))
+ .ReturnsAsync(expectedError);
+
+ // Act
+ var result = await _modelPackageServiceMock.Object.ImportPackageAsync(packagePath);
+
+ // Assert
+ Assert.IsFalse(result.Succeeded);
+ Assert.AreEqual("FILE_NOT_FOUND", result.Code);
+ Assert.AreEqual("模型包文件不存在", result.Message);
+ }
+
+ [TestMethod]
+ public async Task ActivatePackageAsync_ValidPackage_ReturnsSuccess()
+ {
+ // Arrange
+ var packageId = Guid.NewGuid();
+ var expectedResult = new ModelPackageActivationResultDto
+ {
+ ModelPackageId = packageId,
+ Status = ModelPackageActivationStatus.Activated,
+ Message = "激活成功",
+ ActivatedAtUtc = DateTime.UtcNow,
+ ActivatedBy = "TestUser"
+ };
+
+ _modelPackageServiceMock
+ .Setup(x => x.ActivatePackageAsync(packageId, It.IsAny()))
+ .ReturnsAsync(Result.Success(expectedResult));
+
+ // Act
+ var result = await _modelPackageServiceMock.Object.ActivatePackageAsync(packageId);
+
+ // Assert
+ Assert.IsTrue(result.Succeeded);
+ Assert.IsNotNull(result.Data);
+ Assert.AreEqual(ModelPackageActivationStatus.Activated, result.Data.Status);
+ Assert.AreEqual("激活成功", result.Data.Message);
+ Assert.AreEqual("TestUser", result.Data.ActivatedBy);
+ }
+
+ [TestMethod]
+ public async Task ActivatePackageAsync_PackageNotFound_ReturnsFailure()
+ {
+ // Arrange
+ var packageId = Guid.NewGuid();
+ var expectedError = Result.Fail("PACKAGE_NOT_FOUND", "模型包不存在");
+
+ _modelPackageServiceMock
+ .Setup(x => x.ActivatePackageAsync(packageId, It.IsAny()))
+ .ReturnsAsync(expectedError);
+
+ // Act
+ var result = await _modelPackageServiceMock.Object.ActivatePackageAsync(packageId);
+
+ // Assert
+ Assert.IsFalse(result.Succeeded);
+ Assert.AreEqual("PACKAGE_NOT_FOUND", result.Code);
+ Assert.AreEqual("模型包不存在", result.Message);
+ }
+
+ [TestMethod]
+ public async Task ActivatePackageAsync_AlreadyActivated_ReturnsFailure()
+ {
+ // Arrange
+ var packageId = Guid.NewGuid();
+ var expectedError = Result.Fail("ALREADY_ACTIVATED", "模型包已激活");
+
+ _modelPackageServiceMock
+ .Setup(x => x.ActivatePackageAsync(packageId, It.IsAny()))
+ .ReturnsAsync(expectedError);
+
+ // Act
+ var result = await _modelPackageServiceMock.Object.ActivatePackageAsync(packageId);
+
+ // Assert
+ Assert.IsFalse(result.Succeeded);
+ Assert.AreEqual("ALREADY_ACTIVATED", result.Code);
+ Assert.AreEqual("模型包已激活", result.Message);
+ }
+
+ [TestMethod]
+ public async Task ImportAndActivate_CompleteWorkflow_ReturnsSuccess()
+ {
+ // Arrange
+ var packagePath = "test_package.ovpkg";
+ var packageId = Guid.NewGuid();
+
+ var importResult = new ModelPackageImportResultDto
+ {
+ ModelPackageId = packageId,
+ Status = ModelPackageImportStatus.Imported,
+ Message = "导入成功",
+ ImportedAtUtc = DateTime.UtcNow,
+ ImportedBy = "TestUser"
+ };
+
+ var activationResult = new ModelPackageActivationResultDto
+ {
+ ModelPackageId = packageId,
+ Status = ModelPackageActivationStatus.Activated,
+ Message = "激活成功",
+ ActivatedAtUtc = DateTime.UtcNow,
+ ActivatedBy = "TestUser"
+ };
+
+ _modelPackageServiceMock
+ .Setup(x => x.ImportPackageAsync(packagePath, It.IsAny()))
+ .ReturnsAsync(Result.Success(importResult));
+
+ _modelPackageServiceMock
+ .Setup(x => x.ActivatePackageAsync(packageId, It.IsAny()))
+ .ReturnsAsync(Result.Success(activationResult));
+
+ // Act - Import
+ var importStep = await _modelPackageServiceMock.Object.ImportPackageAsync(packagePath);
+
+ // Assert - Import
+ Assert.IsTrue(importStep.Succeeded);
+ Assert.AreEqual(packageId, importStep.Data.ModelPackageId);
+
+ // Act - Activate
+ var activationStep = await _modelPackageServiceMock.Object.ActivatePackageAsync(packageId);
+
+ // Assert - Activate
+ Assert.IsTrue(activationStep.Succeeded);
+ Assert.AreEqual(packageId, activationStep.Data.ModelPackageId);
+ Assert.AreEqual(ModelPackageActivationStatus.Activated, activationStep.Data.Status);
+ }
+
+ [TestMethod]
+ public async Task ImportAndActivate_ImportFailure_StopsWorkflow()
+ {
+ // Arrange
+ var packagePath = "invalid_package.ovpkg";
+ var packageId = Guid.NewGuid();
+
+ var importError = Result.Fail("INVALID_FORMAT", "模型包格式无效");
+
+ _modelPackageServiceMock
+ .Setup(x => x.ImportPackageAsync(packagePath, It.IsAny()))
+ .ReturnsAsync(importError);
+
+ // Act
+ var importStep = await _modelPackageServiceMock.Object.ImportPackageAsync(packagePath);
+
+ // Assert
+ Assert.IsFalse(importStep.Succeeded);
+ Assert.AreEqual("INVALID_FORMAT", importStep.Code);
+
+ // Verify that activation was not attempted
+ _modelPackageServiceMock.Verify(
+ x => x.ActivatePackageAsync(It.IsAny(), It.IsAny()),
+ Times.Never);
+ }
+}
diff --git a/OrpaonVision.SiteApp/Runtime/Contracts/IConsistencyCheckService.cs b/OrpaonVision.SiteApp/Runtime/Contracts/IConsistencyCheckService.cs
index 59b4e1e..effcd57 100644
--- a/OrpaonVision.SiteApp/Runtime/Contracts/IConsistencyCheckService.cs
+++ b/OrpaonVision.SiteApp/Runtime/Contracts/IConsistencyCheckService.cs
@@ -1,3 +1,5 @@
+using OrpaonVision.Core.Results;
+
namespace OrpaonVision.SiteApp.Runtime.Contracts;
///