using System; using System.Collections.Generic; using System.Data; using System.IO; using TeamAAS.Database.Interfaces; using TeamAAS.Database.Models; using TeamAAS.Database.Providers; namespace TeamAAS.Database.Services { /// /// 产品数据库(生产记录 / 运行日志 / 报警历史 / 设备运行数据)。 /// 跟着产品配方走,每产品一个独立数据库文件;也支持配置为 MySQL 远程库。 /// public class ProductDatabase : IDisposable { private readonly IDatabase _db; private readonly DatabaseConfig _config; /// /// 数据库实例 /// public IDatabase Database => _db; /// 当前数据库配置(供配置页展示/编辑;改动后由 ProductDatabaseManager 重建实例生效)。 public DatabaseConfig Config => _config; /// /// 是否连接成功 /// public bool IsConnected => _db?.IsConnected == true; /// /// 当前产品名 /// public string ProductName { get; private set; } /// /// 用配置创建产品数据库(支持 SQLite / MySQL) /// public ProductDatabase(DatabaseConfig config) { _config = config ?? throw new ArgumentNullException(nameof(config)); ProductName = config.Name; // 根据 ProviderType 选择实现 switch ((config.ProviderType ?? "sqlite").ToLowerInvariant()) { case "mysql": _db = new MySqlDatabase(); break; case "sqlite": default: _db = new SqliteDatabase(); break; } _db.Configure(config); } /// /// 用默认 SQLite 创建产品数据库(按产品名建文件,放产品目录下) /// /// 产品目录(如 Products\ProductA\) /// 产品名 public static ProductDatabase CreateDefaultSqlite(string productDir, string productName) { var dbPath = Path.Combine(productDir, "product.db"); var config = new DatabaseConfig { Id = Guid.NewGuid(), Name = productName, ProviderType = "sqlite", Server = dbPath }; return new ProductDatabase(config); } /// /// 初始化:打开连接 + 自动建表 /// public (bool Success, string Message) Initialize() { try { // SQLite 自动建目录 if ((_config.ProviderType ?? "sqlite").ToLowerInvariant() == "sqlite") { var filePath = _config.Server; var dir = Path.GetDirectoryName(filePath); if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); } if (!_db.Open()) return (false, "产品数据库打开失败"); CreateTables(); return (true, "产品数据库初始化成功"); } catch (Exception ex) { return (false, $"产品数据库初始化失败:{ex.Message}"); } } #region 建表 private void CreateTables() { var sqls = new List { // 生产记录 @"CREATE TABLE IF NOT EXISTS prod_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, recipe_name TEXT, batch_no TEXT, serial_no TEXT, result INTEGER DEFAULT 0, start_time TEXT, end_time TEXT, duration_ms INTEGER, operator_name TEXT, remark TEXT, created_at TEXT DEFAULT (datetime('now','localtime')) )", // 运行日志 @"CREATE TABLE IF NOT EXISTS run_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, log_level TEXT DEFAULT 'INFO', module TEXT, message TEXT, detail TEXT, created_at TEXT DEFAULT (datetime('now','localtime')) )", // 报警历史 @"CREATE TABLE IF NOT EXISTS alarm_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, alarm_code TEXT, alarm_name TEXT, alarm_level INTEGER DEFAULT 2, description TEXT, source TEXT, start_time TEXT, end_time TEXT, ack_by TEXT, ack_time TEXT, is_acknowledged INTEGER DEFAULT 0, is_resolved INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime('now','localtime')) )", // 设备运行数据(时序型) @"CREATE TABLE IF NOT EXISTS device_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_name TEXT NOT NULL, data_key TEXT NOT NULL, data_value REAL, value_text TEXT, quality INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now','localtime')) )", // 产品信息表 @"CREATE TABLE IF NOT EXISTS product_info ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_name TEXT NOT NULL UNIQUE, version TEXT, description TEXT, created_at TEXT DEFAULT (datetime('now','localtime')), updated_at TEXT DEFAULT (datetime('now','localtime')) )" }; // 索引 sqls.Add("CREATE INDEX IF NOT EXISTS idx_prod_result ON prod_records(result)"); sqls.Add("CREATE INDEX IF NOT EXISTS idx_prod_time ON prod_records(start_time)"); sqls.Add("CREATE INDEX IF NOT EXISTS idx_prod_serial ON prod_records(serial_no)"); sqls.Add("CREATE INDEX IF NOT EXISTS idx_log_level ON run_logs(log_level)"); sqls.Add("CREATE INDEX IF NOT EXISTS idx_log_time ON run_logs(created_at)"); sqls.Add("CREATE INDEX IF NOT EXISTS idx_log_module ON run_logs(module)"); sqls.Add("CREATE INDEX IF NOT EXISTS idx_alarm_level ON alarm_history(alarm_level)"); sqls.Add("CREATE INDEX IF NOT EXISTS idx_alarm_time ON alarm_history(start_time)"); sqls.Add("CREATE INDEX IF NOT EXISTS idx_alarm_resolved ON alarm_history(is_resolved)"); sqls.Add("CREATE INDEX IF NOT EXISTS idx_device_name ON device_data(device_name)"); sqls.Add("CREATE INDEX IF NOT EXISTS idx_device_time ON device_data(created_at)"); sqls.Add("CREATE INDEX IF NOT EXISTS idx_device_key ON device_data(data_key)"); _db.ExecuteTransaction(sqls); // 写入产品信息 var productInfo = _db.ExecuteScalar( "SELECT COUNT(*) FROM product_info WHERE product_name = @n", new Dictionary {{"@n", ProductName}}); if (Convert.ToInt32(productInfo) == 0) { _db.ExecuteNonQuery( "INSERT INTO product_info (product_name, description) VALUES (@n, @d)", new Dictionary { {"@n", ProductName}, {"@d", $"产品[{ProductName}]的运行数据库"} }); } } #endregion #region 生产记录 /// /// 写入一条生产记录(开始) /// /// 记录ID public int StartProduction(string recipeName, string batchNo, string serialNo, string operatorName = null) { var sql = @"INSERT INTO prod_records (recipe_name, batch_no, serial_no, start_time, operator_name) VALUES (@r, @b, @s, datetime('now','localtime'), @o)"; var p = new Dictionary { {"@r", (object)recipeName ?? DBNull.Value}, {"@b", (object)batchNo ?? DBNull.Value}, {"@s", (object)serialNo ?? DBNull.Value}, {"@o", (object)operatorName ?? DBNull.Value} }; _db.ExecuteNonQuery(sql, p); return Convert.ToInt32(_db.ExecuteScalar("SELECT last_insert_rowid()")); } /// /// 完成生产记录 /// public void FinishProduction(int recordId, bool success, string remark = null) { var sql = @"UPDATE prod_records SET result = @r, end_time = datetime('now','localtime'), duration_ms = CAST((julianday('now','localtime') - julianday(start_time)) * 86400000 AS INTEGER), remark = @rk WHERE id = @id"; var p = new Dictionary { {"@id", recordId}, {"@r", success ? 1 : 0}, {"@rk", (object)remark ?? DBNull.Value} }; _db.ExecuteNonQuery(sql, p); } /// /// 查询生产记录统计 /// public (int Total, int Ok, int Ng, double Yield) GetProductionStats(DateTime? startTime = null, DateTime? endTime = null) { var conditions = new List(); var parameters = new Dictionary(); if (startTime.HasValue) { conditions.Add("start_time >= @st"); parameters["@st"] = startTime.Value.ToString("yyyy-MM-dd HH:mm:ss"); } if (endTime.HasValue) { conditions.Add("start_time <= @et"); parameters["@et"] = endTime.Value.ToString("yyyy-MM-dd HH:mm:ss"); } var where = conditions.Count > 0 ? "WHERE " + string.Join(" AND ", conditions) : ""; var total = Convert.ToInt32( _db.ExecuteScalar($"SELECT COUNT(*) FROM prod_records {where}", parameters)); var ok = Convert.ToInt32( _db.ExecuteScalar($"SELECT COUNT(*) FROM prod_records {where} AND result = 1", parameters)); var ng = total - ok; var yield = total > 0 ? (double)ok / total * 100 : 0; return (total, ok, ng, Math.Round(yield, 2)); } #endregion #region 运行日志 /// /// 写入运行日志 /// public void WriteLog(string level, string module, string message, string detail = null) { try { var sql = @"INSERT INTO run_logs (log_level, module, message, detail) VALUES (@l, @m, @msg, @d)"; var p = new Dictionary { {"@l", level ?? "INFO"}, {"@m", (object)module ?? DBNull.Value}, {"@msg", message ?? ""}, {"@d", (object)detail ?? DBNull.Value} }; _db.ExecuteNonQuery(sql, p); } catch { /* 日志写入失败不抛 */ } } /// /// 便捷方法:INFO /// public void LogInfo(string module, string message, string detail = null) => WriteLog("INFO", module, message, detail); /// /// 便捷方法:WARN /// public void LogWarn(string module, string message, string detail = null) => WriteLog("WARN", module, message, detail); /// /// 便捷方法:ERROR /// public void LogError(string module, string message, string detail = null) => WriteLog("ERROR", module, message, detail); #endregion #region 报警 /// /// 触发报警 /// /// 报警记录ID public int RaiseAlarm(string code, string name, int level, string description = null, string source = null) { var sql = @"INSERT INTO alarm_history (alarm_code, alarm_name, alarm_level, description, source, start_time) VALUES (@c, @n, @l, @d, @s, datetime('now','localtime'))"; var p = new Dictionary { {"@c", code}, {"@n", name}, {"@l", level}, {"@d", (object)description ?? DBNull.Value}, {"@s", (object)source ?? DBNull.Value} }; _db.ExecuteNonQuery(sql, p); return Convert.ToInt32(_db.ExecuteScalar("SELECT last_insert_rowid()")); } /// /// 确认报警 /// public void AcknowledgeAlarm(int alarmId, string ackBy = null) { var sql = @"UPDATE alarm_history SET is_acknowledged = 1, ack_by = @a, ack_time = datetime('now','localtime') WHERE id = @id"; var p = new Dictionary { {"@id", alarmId}, {"@a", (object)ackBy ?? DBNull.Value} }; _db.ExecuteNonQuery(sql, p); } /// /// 消除报警 /// public void ResolveAlarm(int alarmId) { var sql = @"UPDATE alarm_history SET is_resolved = 1, end_time = datetime('now','localtime') WHERE id = @id"; _db.ExecuteNonQuery(sql, new Dictionary {{"@id", alarmId}}); } /// /// 获取未消除报警数 /// public int GetActiveAlarmCount() { return Convert.ToInt32( _db.ExecuteScalar("SELECT COUNT(*) FROM alarm_history WHERE is_resolved = 0")); } #endregion #region 设备数据 /// /// 写入设备运行数据 /// public void WriteDeviceData(string deviceName, string dataKey, double? value = null, string valueText = null, int quality = 1) { try { var sql = @"INSERT INTO device_data (device_name, data_key, data_value, value_text, quality) VALUES (@d, @k, @v, @vt, @q)"; var p = new Dictionary { {"@d", deviceName}, {"@k", dataKey}, {"@v", (object)value ?? DBNull.Value}, {"@vt", (object)valueText ?? DBNull.Value}, {"@q", quality} }; _db.ExecuteNonQuery(sql, p); } catch { /* 数据写入失败不抛 */ } } /// /// 批量写入设备数据 /// public void WriteDeviceDataBatch(IEnumerable<(string deviceName, string key, double? value, string valueText)> dataList) { if (dataList == null) return; var sqls = new List(); // 简单拼装(内部数据,安全风险低) foreach (var d in dataList) { var v = d.value.HasValue ? d.value.Value.ToString(System.Globalization.CultureInfo.InvariantCulture) : "NULL"; var vt = d.valueText != null ? $"'{d.valueText.Replace("'", "''")}'" : "NULL"; sqls.Add($@"INSERT INTO device_data (device_name, data_key, data_value, value_text) VALUES ('{d.deviceName.Replace("'", "''")}', '{d.key.Replace("'", "''")}', {v}, {vt})"); } if (sqls.Count > 0) _db.ExecuteTransaction(sqls); } /// /// 查询设备某数据点的历史 /// public DataTable QueryDeviceHistory(string deviceName, string dataKey, DateTime? startTime = null, DateTime? endTime = null, int limit = 1000) { var conditions = new List {"device_name = @d", "data_key = @k"}; var parameters = new Dictionary { {"@d", deviceName}, {"@k", dataKey} }; if (startTime.HasValue) { conditions.Add("created_at >= @st"); parameters["@st"] = startTime.Value.ToString("yyyy-MM-dd HH:mm:ss"); } if (endTime.HasValue) { conditions.Add("created_at <= @et"); parameters["@et"] = endTime.Value.ToString("yyyy-MM-dd HH:mm:ss"); } var where = "WHERE " + string.Join(" AND ", conditions); var sql = $@" SELECT * FROM device_data {where} ORDER BY id DESC LIMIT @lim"; parameters["@lim"] = limit; var dt = _db.ExecuteQuery(sql, parameters); // 反序变成时间正序 var result = dt.Clone(); for (int i = dt.Rows.Count - 1; i >= 0; i--) { result.ImportRow(dt.Rows[i]); } return result; } #endregion #region IDisposable private bool _disposed; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { try { _db?.Dispose(); } catch { } } _disposed = true; } ~ProductDatabase() { Dispose(false); } #endregion } }