Files
XplorePlane/XP.Hardware.RaySource/Services/FilamentLifetimeService.cs
T

578 lines
26 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 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
{
/// <summary>
/// 灯丝寿命管理服务实现 | Filament Lifetime Management Service Implementation
/// 负责灯丝使用时长的记录、计算、异常恢复和预警判断
/// Responsible for filament usage duration recording, calculation, anomaly recovery, and warning evaluation
/// </summary>
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();
/// <inheritdoc/>
public bool IsInitialized { get; private set; }
/// <inheritdoc/>
public bool IsFilamentOn => _filamentStartTime != null;
#region | Constructor
/// <summary>
/// 构造函数,注入依赖 | Constructor with dependency injection
/// </summary>
/// <param name="dbContext">数据库上下文 | Database context</param>
/// <param name="config">射线源配置 | Ray source configuration</param>
/// <param name="logger">日志服务 | Logger service</param>
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<FilamentLifetimeService>() ?? throw new ArgumentNullException(nameof(logger));
}
#endregion
#region | Initialization
/// <inheritdoc/>
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;
}
}
/// <summary>
/// 创建数据库表 | Create database tables
/// </summary>
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;
}
}
/// <summary>
/// 异常恢复:关闭未结束的流水记录 | Anomaly recovery: close unclosed usage log records
/// </summary>
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<string, object>
{
{ "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 设为 StartTimeDurationSeconds 设为 0Status 设为 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<string, object>
{
{ "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);
}
}
/// <summary>
/// 基于流水表重算累计寿命 | Recalculate accumulated lifetime based on usage logs
/// </summary>
private void RecalculateTotalLifeSeconds()
{
try
{
var sumSql = @"
SELECT COALESCE(SUM(DurationSeconds), 0) FROM RaySourceFilamentUsageLogs
WHERE SourceType = @SourceType AND SerialNumber = @SerialNumber";
var sumParams = new Dictionary<string, object>
{
{ "SourceType", _sourceType },
{ "SerialNumber", _serialNumber }
};
var (sumResult, totalSeconds) = _dbContext.ExecuteScalar<double>(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<string, object>
{
{ "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
/// <inheritdoc/>
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<string, object>
{
{ "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<string, object>
{
{ "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
}
/// <inheritdoc/>
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<string, object>
{
{ "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<string, object>
{
{ "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<string, object>
{
{ "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<string, object>
{
{ "SourceType", _sourceType },
{ "SerialNumber", _serialNumber }
};
var (totalResult, totalData) = _dbContext.ExecuteScalar<double>(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
/// <inheritdoc/>
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<string, object>
{
{ "SourceType", _sourceType },
{ "SerialNumber", _serialNumber }
};
var (result, totalSeconds) = _dbContext.ExecuteScalar<double>(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;
}
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public double GetThresholdSeconds()
{
return _thresholdSeconds;
}
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
public bool ShouldShowLifetimeWarning()
{
return GetLifetimePercentage() >= 90;
}
#endregion
}
}