using System; using System.Collections.Generic; using System.Data; using XP.Common.Database.Interfaces; using XP.Common.Logging.Interfaces; using XP.Hardware.RaySource.Config; namespace XP.Hardware.RaySource.Services { /// /// 灯丝寿命管理服务实现 | Filament Lifetime Management Service Implementation /// 负责灯丝使用时长的记录、计算、异常恢复和预警判断 /// Responsible for filament usage duration recording, calculation, anomaly recovery, and warning evaluation /// public class FilamentLifetimeService : IFilamentLifetimeService { private readonly IDbContext _dbContext; private readonly RaySourceConfig _config; private readonly ILoggerService _logger; // 内存状态 | In-memory state private DateTime? _filamentStartTime; private string _sourceType = ""; private string _serialNumber = ""; private double _thresholdSeconds; private readonly object _filamentLock = new object(); /// public bool IsInitialized { get; private set; } /// public bool IsFilamentOn => _filamentStartTime != null; #region 构造函数 | Constructor /// /// 构造函数,注入依赖 | Constructor with dependency injection /// /// 数据库上下文 | Database context /// 射线源配置 | Ray source configuration /// 日志服务 | Logger service public FilamentLifetimeService(IDbContext dbContext, RaySourceConfig config, ILoggerService logger) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); _config = config ?? throw new ArgumentNullException(nameof(config)); _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); } #endregion #region 初始化 | Initialization /// public bool Initialize() { try { // 1. 读取配置 | Read configuration _sourceType = _config.SourceType; _serialNumber = _config.SerialNumber; _thresholdSeconds = _config.TotalLifeThreshold * 3600; // 2. 验证配置(SourceType 或 SerialNumber 为空时阻止初始化) // Validate configuration (block initialization if SourceType or SerialNumber is empty) if (string.IsNullOrWhiteSpace(_sourceType) || string.IsNullOrWhiteSpace(_serialNumber)) { _logger.Error(null, "灯丝寿命服务初始化失败:SourceType 或 SerialNumber 为空,SourceType={SourceType},SerialNumber={SerialNumber} | " + "Filament lifetime service initialization failed: SourceType or SerialNumber is empty, SourceType={SourceType}, SerialNumber={SerialNumber}", _sourceType ?? "", _serialNumber ?? ""); IsInitialized = false; return false; } // 3. 建表(CREATE TABLE IF NOT EXISTS)| Create tables if (!CreateTables()) { IsInitialized = false; return false; } // 4. 异常恢复(关闭未结束记录)| Anomaly recovery (close unclosed records) RecoverUnclosedRecords(); // 5. 重算累计寿命 | Recalculate accumulated lifetime RecalculateTotalLifeSeconds(); IsInitialized = true; _logger.Info("灯丝寿命服务初始化成功,SourceType={SourceType},SerialNumber={SerialNumber},阈值={Threshold}小时 | " + "Filament lifetime service initialized successfully, SourceType={SourceType}, SerialNumber={SerialNumber}, threshold={Threshold} hours", _sourceType, _serialNumber, _config.TotalLifeThreshold); return true; } catch (Exception ex) { _logger.Error(ex, "灯丝寿命服务初始化异常 | Filament lifetime service initialization exception: {Message}", ex.Message); IsInitialized = false; return false; } } /// /// 创建数据库表 | Create database tables /// private bool CreateTables() { try { // 创建累计统计表 | Create lifetime statistics table var createStatsSql = @" CREATE TABLE IF NOT EXISTS RaySourceFilamentLifetimeStatistics ( Id INTEGER PRIMARY KEY AUTOINCREMENT, SourceType TEXT NOT NULL, SerialNumber TEXT NOT NULL, TotalLifeSeconds REAL NOT NULL DEFAULT 0, LastUpdateTime TEXT NOT NULL DEFAULT '1970-01-01T00:00:00Z' ); CREATE UNIQUE INDEX IF NOT EXISTS idx_lifetime_serial ON RaySourceFilamentLifetimeStatistics(SerialNumber); CREATE INDEX IF NOT EXISTS idx_lifetime_source_type ON RaySourceFilamentLifetimeStatistics(SourceType);"; var statsResult = _dbContext.ExecuteNonQuery(createStatsSql); if (!statsResult.IsSuccess) { _logger.Error(statsResult.Exception, "创建统计表失败:{Message} | Failed to create statistics table: {Message}", statsResult.Message); return false; } // 创建使用流水表 | Create usage logs table var createLogsSql = @" CREATE TABLE IF NOT EXISTS RaySourceFilamentUsageLogs ( Id INTEGER PRIMARY KEY AUTOINCREMENT, SourceType TEXT NOT NULL, SerialNumber TEXT NOT NULL, StartTime TEXT NOT NULL, EndTime TEXT, DurationSeconds REAL NOT NULL DEFAULT 0, Status INTEGER NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_usage_serial ON RaySourceFilamentUsageLogs(SerialNumber);"; var logsResult = _dbContext.ExecuteNonQuery(createLogsSql); if (!logsResult.IsSuccess) { _logger.Error(logsResult.Exception, "创建流水表失败:{Message} | Failed to create usage logs table: {Message}", logsResult.Message); return false; } return true; } catch (Exception ex) { _logger.Error(ex, "建表异常 | Exception creating tables: {Message}", ex.Message); return false; } } /// /// 异常恢复:关闭未结束的流水记录 | Anomaly recovery: close unclosed usage log records /// private void RecoverUnclosedRecords() { try { var querySql = @" SELECT Id, StartTime FROM RaySourceFilamentUsageLogs WHERE SourceType = @SourceType AND SerialNumber = @SerialNumber AND EndTime IS NULL"; var queryParams = new Dictionary { { "SourceType", _sourceType }, { "SerialNumber", _serialNumber } }; var (queryResult, dataTable) = _dbContext.ExecuteDataTable(querySql, queryParams); if (!queryResult.IsSuccess || dataTable == null || dataTable.Rows.Count == 0) { return; } _logger.Warn("检测到 {Count} 条未关闭的灯丝使用记录,开始异常恢复 | " + "Detected {Count} unclosed filament usage records, starting anomaly recovery", dataTable.Rows.Count); foreach (DataRow row in dataTable.Rows) { var id = Convert.ToInt64(row["Id"]); var startTimeStr = row["StartTime"]?.ToString() ?? ""; // 将未关闭记录的 EndTime 设为 StartTime,DurationSeconds 设为 0,Status 设为 1(异常中断) // Set unclosed record's EndTime to StartTime, DurationSeconds to 0, Status to 1 (abnormal interruption) var updateSql = @" UPDATE RaySourceFilamentUsageLogs SET EndTime = StartTime, DurationSeconds = 0, Status = 1 WHERE Id = @Id"; var updateParams = new Dictionary { { "Id", id } }; _dbContext.ExecuteNonQuery(updateSql, updateParams); _logger.Warn("异常恢复:关闭未结束记录,SourceType={SourceType},SerialNumber={SerialNumber},StartTime={StartTime} | " + "Anomaly recovery: closed unclosed record, SourceType={SourceType}, SerialNumber={SerialNumber}, StartTime={StartTime}", _sourceType, _serialNumber, startTimeStr); } } catch (Exception ex) { _logger.Error(ex, "异常恢复失败 | Anomaly recovery failed: {Message}", ex.Message); } } /// /// 基于流水表重算累计寿命 | Recalculate accumulated lifetime based on usage logs /// private void RecalculateTotalLifeSeconds() { try { var sumSql = @" SELECT COALESCE(SUM(DurationSeconds), 0) FROM RaySourceFilamentUsageLogs WHERE SourceType = @SourceType AND SerialNumber = @SerialNumber"; var sumParams = new Dictionary { { "SourceType", _sourceType }, { "SerialNumber", _serialNumber } }; var (sumResult, totalSeconds) = _dbContext.ExecuteScalar(sumSql, sumParams); if (!sumResult.IsSuccess) { _logger.Error(sumResult.Exception, "重算累计寿命失败:查询 DurationSeconds 总和出错,{Message} | " + "Failed to recalculate lifetime: error querying DurationSeconds sum, {Message}", sumResult.Message); return; } var nowUtc = DateTime.UtcNow.ToString("o"); // 更新或插入统计表 | Update or insert statistics table var upsertSql = @" INSERT INTO RaySourceFilamentLifetimeStatistics (SourceType, SerialNumber, TotalLifeSeconds, LastUpdateTime) VALUES (@SourceType, @SerialNumber, @TotalLifeSeconds, @LastUpdateTime) ON CONFLICT(SerialNumber) DO UPDATE SET TotalLifeSeconds = @TotalLifeSeconds, LastUpdateTime = @LastUpdateTime"; var updateParams = new Dictionary { { "TotalLifeSeconds", totalSeconds }, { "LastUpdateTime", nowUtc }, { "SourceType", _sourceType }, { "SerialNumber", _serialNumber } }; var updateResult = _dbContext.ExecuteNonQuery(upsertSql, updateParams); if (!updateResult.IsSuccess) { _logger.Error(updateResult.Exception, "重算累计寿命失败:更新统计表出错,{Message} | " + "Failed to recalculate lifetime: error updating statistics table, {Message}", updateResult.Message); } } catch (Exception ex) { _logger.Error(ex, "重算累计寿命异常 | Exception recalculating lifetime: {Message}", ex.Message); } } #endregion #region 灯丝开启与关闭 | Filament Start and Stop /// public bool StartFilamentUsage() { if (!IsInitialized) { _logger.Warn("灯丝寿命服务未初始化,无法开始记录 | Filament lifetime service not initialized, cannot start recording"); return false; } lock (_filamentLock) { // 锁内再次检查,防止并发重复开始 | Double-check inside lock to prevent concurrent duplicate start if (_filamentStartTime != null) { _logger.Warn("灯丝已处于开启状态,忽略重复开始请求 | Filament already on, ignoring duplicate start request"); return false; } try { // 1. 确保统计表中存在当前设备记录 | Ensure current device record exists in statistics table var ensureStatsSql = @" INSERT OR IGNORE INTO RaySourceFilamentLifetimeStatistics (SourceType, SerialNumber, TotalLifeSeconds, LastUpdateTime) VALUES (@SourceType, @SerialNumber, 0, @LastUpdateTime)"; var ensureParams = new Dictionary { { "SourceType", _sourceType }, { "SerialNumber", _serialNumber }, { "LastUpdateTime", DateTime.UtcNow.ToString("o") } }; var ensureResult = _dbContext.ExecuteNonQuery(ensureStatsSql, ensureParams); if (!ensureResult.IsSuccess) { _logger.Error(ensureResult.Exception, "确保统计记录存在失败:{Message},SourceType={SourceType},SerialNumber={SerialNumber} | " + "Failed to ensure statistics record exists: {Message}, SourceType={SourceType}, SerialNumber={SerialNumber}", ensureResult.Message, _sourceType, _serialNumber); return false; } // 2. 向流水表插入新记录 | Insert new usage log record var startTime = DateTime.UtcNow; var startTimeStr = startTime.ToString("o"); var insertLogSql = @" INSERT INTO RaySourceFilamentUsageLogs (SourceType, SerialNumber, StartTime, EndTime, DurationSeconds, Status) VALUES (@SourceType, @SerialNumber, @StartTime, NULL, 0, 0)"; var insertParams = new Dictionary { { "SourceType", _sourceType }, { "SerialNumber", _serialNumber }, { "StartTime", startTimeStr } }; var insertResult = _dbContext.ExecuteNonQuery(insertLogSql, insertParams); if (!insertResult.IsSuccess) { _logger.Error(insertResult.Exception, "插入灯丝使用流水记录失败:{Message},SourceType={SourceType},SerialNumber={SerialNumber} | " + "Failed to insert filament usage log: {Message}, SourceType={SourceType}, SerialNumber={SerialNumber}", insertResult.Message, _sourceType, _serialNumber); return false; } // 3. 记录内存起始时间(仅在数据库插入成功后更新)| Record in-memory start time _filamentStartTime = startTime; _logger.Info("灯丝使用开始,SourceType={SourceType},SerialNumber={SerialNumber},StartTime={StartTime} | " + "Filament usage started, SourceType={SourceType}, SerialNumber={SerialNumber}, StartTime={StartTime}", _sourceType, _serialNumber, startTimeStr); return true; } catch (Exception ex) { _logger.Error(ex, "开始灯丝使用记录异常,SourceType={SourceType},SerialNumber={SerialNumber} | " + "Exception starting filament usage recording, SourceType={SourceType}, SerialNumber={SerialNumber}", _sourceType, _serialNumber); return false; } } // end lock } /// public bool StopFilamentUsage() { if (!IsInitialized) { _logger.Warn("灯丝寿命服务未初始化,无法停止记录 | Filament lifetime service not initialized, cannot stop recording"); return false; } lock (_filamentLock) { // 锁内再次检查,防止并发重复停止 | Double-check inside lock to prevent concurrent duplicate stop if (_filamentStartTime == null) { _logger.Warn("灯丝已处于关闭状态,忽略重复停止请求 | Filament already off, ignoring duplicate stop request"); return false; } try { // 1. 查找当前设备最近一条未关闭的记录 | Find the most recent unclosed record var querySql = @" SELECT Id, StartTime FROM RaySourceFilamentUsageLogs WHERE SourceType = @SourceType AND SerialNumber = @SerialNumber AND EndTime IS NULL ORDER BY Id DESC LIMIT 1"; var queryParams = new Dictionary { { "SourceType", _sourceType }, { "SerialNumber", _serialNumber } }; var (queryResult, dataTable) = _dbContext.ExecuteDataTable(querySql, queryParams); if (!queryResult.IsSuccess || dataTable == null || dataTable.Rows.Count == 0) { _logger.Warn("未找到当前设备的未结束流水记录,SourceType={SourceType},SerialNumber={SerialNumber} | " + "No unclosed usage log found, SourceType={SourceType}, SerialNumber={SerialNumber}", _sourceType, _serialNumber); return false; } var row = dataTable.Rows[0]; var recordId = Convert.ToInt64(row["Id"]); var startTimeStr = row["StartTime"]?.ToString() ?? ""; // 2. 计算 DurationSeconds | Calculate DurationSeconds var endTime = DateTime.UtcNow; var endTimeStr = endTime.ToString("o"); double durationSeconds = 0; if (DateTime.TryParse(startTimeStr, null, System.Globalization.DateTimeStyles.RoundtripKind, out var startTime)) { durationSeconds = (endTime - startTime).TotalSeconds; if (durationSeconds < 0) durationSeconds = 0; } // 3. 顺序更新流水表和统计表(ExecuteNonQuery 不支持外部事务参数,改为顺序执行) // Sequential update: usage log then statistics // 3.1 更新流水记录 | Update usage log var updateLogSql = @" UPDATE RaySourceFilamentUsageLogs SET EndTime = @EndTime, DurationSeconds = @DurationSeconds, Status = 1 WHERE Id = @Id"; var updateLogParams = new Dictionary { { "EndTime", endTimeStr }, { "DurationSeconds", durationSeconds }, { "Id", recordId } }; var updateLogResult = _dbContext.ExecuteNonQuery(updateLogSql, updateLogParams); if (!updateLogResult.IsSuccess) { _logger.Error(updateLogResult.Exception, "更新流水记录失败:{Message} | Failed to update usage log: {Message}", updateLogResult.Message); return false; } // 3.2 累加统计表 | Accumulate statistics var updateStatsSql = @" UPDATE RaySourceFilamentLifetimeStatistics SET TotalLifeSeconds = TotalLifeSeconds + @DurationSeconds, LastUpdateTime = @LastUpdateTime WHERE SourceType = @SourceType AND SerialNumber = @SerialNumber"; var updateStatsParams = new Dictionary { { "DurationSeconds", durationSeconds }, { "LastUpdateTime", endTimeStr }, { "SourceType", _sourceType }, { "SerialNumber", _serialNumber } }; var updateStatsResult = _dbContext.ExecuteNonQuery(updateStatsSql, updateStatsParams); if (!updateStatsResult.IsSuccess) { _logger.Error(updateStatsResult.Exception, "更新统计记录失败:{Message} | Failed to update statistics: {Message}", updateStatsResult.Message); // 流水已更新但统计失败,记录警告(下次 RecalculateTotalLifeSeconds 可恢复) _logger.Warn("统计表更新失败但流水记录已关闭,可通过重算恢复 | Statistics update failed but usage log closed, recoverable via recalculation"); } // 4. 清除内存起始时间 | Clear in-memory start time _filamentStartTime = null; // 5. 查询更新后的 TotalLifeSeconds 用于日志 | Query updated TotalLifeSeconds for logging var totalLifeSeconds = 0.0; var totalSql = @" SELECT TotalLifeSeconds FROM RaySourceFilamentLifetimeStatistics WHERE SourceType = @SourceType AND SerialNumber = @SerialNumber"; var totalParams = new Dictionary { { "SourceType", _sourceType }, { "SerialNumber", _serialNumber } }; var (totalResult, totalData) = _dbContext.ExecuteScalar(totalSql, totalParams); if (totalResult.IsSuccess) { totalLifeSeconds = totalData; } _logger.Info("灯丝使用结束,SourceType={SourceType},SerialNumber={SerialNumber},DurationSeconds={DurationSeconds},TotalLifeSeconds={TotalLifeSeconds} | " + "Filament usage stopped, SourceType={SourceType}, SerialNumber={SerialNumber}, DurationSeconds={DurationSeconds}, TotalLifeSeconds={TotalLifeSeconds}", _sourceType, _serialNumber, durationSeconds, totalLifeSeconds); return true; } catch (Exception ex) { _logger.Error(ex, "停止灯丝使用记录异常,SourceType={SourceType},SerialNumber={SerialNumber} | " + "Exception stopping filament usage recording, SourceType={SourceType}, SerialNumber={SerialNumber}", _sourceType, _serialNumber); return false; } } // end lock } #endregion #region 查询与计算 | Query and Calculation /// public double GetTotalLifeSeconds() { if (!IsInitialized) { _logger.Warn("服务未初始化,返回默认值 0 | Service not initialized, returning default value 0"); return 0; } try { var sql = @" SELECT COALESCE(TotalLifeSeconds, 0) FROM RaySourceFilamentLifetimeStatistics WHERE SourceType = @SourceType AND SerialNumber = @SerialNumber"; var parameters = new Dictionary { { "SourceType", _sourceType }, { "SerialNumber", _serialNumber } }; var (result, totalSeconds) = _dbContext.ExecuteScalar(sql, parameters); if (!result.IsSuccess) { _logger.Error(result.Exception, "查询累计使用秒数失败,{Message} | Failed to query total life seconds, {Message}", result.Message); return 0; } return totalSeconds; } catch (Exception ex) { _logger.Error(ex, "查询累计使用秒数异常,SourceType={SourceType},SerialNumber={SerialNumber} | " + "Exception querying total life seconds, SourceType={SourceType}, SerialNumber={SerialNumber}", _sourceType, _serialNumber); return 0; } } /// public double GetCurrentTotalLifeSeconds() { var dbSeconds = GetTotalLifeSeconds(); // 如果灯丝正在运行,加上本次运行时长 | If filament is on, add current session duration if (_filamentStartTime.HasValue) { var currentSessionSeconds = (DateTime.UtcNow - _filamentStartTime.Value).TotalSeconds; dbSeconds += currentSessionSeconds; } return dbSeconds; } /// public double GetThresholdSeconds() { return _thresholdSeconds; } /// public double GetLifetimePercentage() { // 处理阈值为 0 的边界情况,避免除零 | Handle zero threshold to avoid division by zero if (_thresholdSeconds <= 0) { return 100; } var totalSeconds = GetCurrentTotalLifeSeconds(); return Math.Min(totalSeconds / _thresholdSeconds * 100, 100); } /// public bool ShouldShowLifetimeWarning() { return GetLifetimePercentage() >= 90; } #endregion } }