| | |
| | | DriverType TEXT NOT NULL, |
| | | Transport TEXT NOT NULL, |
| | | Endpoint TEXT NOT NULL, |
| | | ExecutionMode TEXT NOT NULL DEFAULT 'Local', |
| | | AuthorityId TEXT NOT NULL DEFAULT '', |
| | | BrokerId TEXT NOT NULL DEFAULT '', |
| | | OwnerProcessId TEXT NOT NULL DEFAULT '', |
| | | PhysicalSessionKey TEXT NOT NULL DEFAULT '', |
| | | EffectiveVersion TEXT NOT NULL DEFAULT '', |
| | | CreatedAt TEXT NOT NULL, |
| | | UpdatedAt TEXT NOT NULL |
| | |
| | | }; |
| | | |
| | | helper.ExecuteBatchSql(sqls); |
| | | |
| | | // V3.0 §05:执行位置字段迁移(旧库补列,新库已在建表语句中包含) |
| | | MigrateExecutionColumns(helper); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"初始化设备配置数据库失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// V3.0:旧版 DeviceDefinition 表补充执行位置五列(SQLite 无行级锁假设,逐列 PRAGMA 检查后 ALTER) |
| | | /// </summary> |
| | | private static void MigrateExecutionColumns(HMI.Core.Util.SqliteHelper helper) |
| | | { |
| | | var dt = helper.GetDataTable("PRAGMA table_info(DeviceDefinition);"); |
| | | var existing = new HashSet<string>( |
| | | dt.Rows.Cast<DataRow>().Select(r => r["name"].ToString()), StringComparer.OrdinalIgnoreCase); |
| | | |
| | | var additions = new (string Col, string Ddl)[] |
| | | { |
| | | ("ExecutionMode", "ALTER TABLE DeviceDefinition ADD COLUMN ExecutionMode TEXT NOT NULL DEFAULT 'Local';"), |
| | | ("AuthorityId", "ALTER TABLE DeviceDefinition ADD COLUMN AuthorityId TEXT NOT NULL DEFAULT '';"), |
| | | ("BrokerId", "ALTER TABLE DeviceDefinition ADD COLUMN BrokerId TEXT NOT NULL DEFAULT '';"), |
| | | ("OwnerProcessId", "ALTER TABLE DeviceDefinition ADD COLUMN OwnerProcessId TEXT NOT NULL DEFAULT '';"), |
| | | ("PhysicalSessionKey", "ALTER TABLE DeviceDefinition ADD COLUMN PhysicalSessionKey TEXT NOT NULL DEFAULT '';"), |
| | | }; |
| | | foreach (var (col, ddl) in additions) |
| | | { |
| | | if (!existing.Contains(col)) |
| | | { |
| | | helper.ExecuteSql(ddl); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | device.UpdatedAt = DateTime.Now; |
| | | |
| | | var sql = $@"INSERT OR REPLACE INTO DeviceDefinition |
| | | (DeviceId, Kind, DriverType, Transport, Endpoint, EffectiveVersion, CreatedAt, UpdatedAt) |
| | | (DeviceId, Kind, DriverType, Transport, Endpoint, ExecutionMode, AuthorityId, BrokerId, OwnerProcessId, PhysicalSessionKey, EffectiveVersion, CreatedAt, UpdatedAt) |
| | | VALUES ( |
| | | '{Escape(device.DeviceId)}', |
| | | '{device.Kind}', |
| | | '{Escape(device.DriverType)}', |
| | | '{device.Transport}', |
| | | '{Escape(device.Endpoint)}', |
| | | '{device.ExecutionMode}', |
| | | '{Escape(device.AuthorityId)}', |
| | | '{Escape(device.BrokerId)}', |
| | | '{Escape(device.OwnerProcessId)}', |
| | | '{Escape(device.PhysicalSessionKey)}', |
| | | '{Escape(device.EffectiveVersion)}', |
| | | '{device.CreatedAt:yyyy-MM-dd HH:mm:ss}', |
| | | '{device.UpdatedAt:yyyy-MM-dd HH:mm:ss}' |
| | |
| | | DriverType = row["DriverType"].ToString() ?? string.Empty, |
| | | Transport = Enum.TryParse<TransportType>(row["Transport"].ToString(), out var tp) ? tp : TransportType.Tcp, |
| | | Endpoint = row["Endpoint"].ToString() ?? string.Empty, |
| | | ExecutionMode = HasColumn(row.Table, "ExecutionMode") && Enum.TryParse<ExecutionModeKind>(row["ExecutionMode"].ToString(), out var em) |
| | | ? em : ExecutionModeKind.Local, |
| | | AuthorityId = HasColumn(row.Table, "AuthorityId") ? row["AuthorityId"].ToString() ?? string.Empty : string.Empty, |
| | | BrokerId = HasColumn(row.Table, "BrokerId") ? row["BrokerId"].ToString() ?? string.Empty : string.Empty, |
| | | OwnerProcessId = HasColumn(row.Table, "OwnerProcessId") ? row["OwnerProcessId"].ToString() ?? string.Empty : string.Empty, |
| | | PhysicalSessionKey = HasColumn(row.Table, "PhysicalSessionKey") ? row["PhysicalSessionKey"].ToString() ?? string.Empty : string.Empty, |
| | | EffectiveVersion = row["EffectiveVersion"].ToString() ?? string.Empty, |
| | | CreatedAt = DateTime.TryParse(row["CreatedAt"].ToString(), out var ca) ? ca : DateTime.Now, |
| | | UpdatedAt = DateTime.TryParse(row["UpdatedAt"].ToString(), out var ua) ? ua : DateTime.Now, |
| | | }); |
| | | } |
| | | return list; |
| | | } |
| | | |
| | | private static bool HasColumn(DataTable dt, string columnName) |
| | | { |
| | | return dt.Columns.Cast<DataColumn>().Any(c => c.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase)); |
| | | } |
| | | |
| | | #endregion |
| | |
| | | var deviceDt = helper.GetDataTable("SELECT * FROM DeviceDefinition ORDER BY DeviceId;"); |
| | | foreach (DataRow row in deviceDt.Rows) |
| | | { |
| | | sb.Append($"DEVICE:{row["DeviceId"]}|{row["Kind"]}|{row["DriverType"]}|{row["Transport"]}|{row["Endpoint"]};"); |
| | | // V3.0:执行位置字段进入配置哈希(INV-04:Broker 连接携 configHash 对账); |
| | | // OwnerProcessId 属运行期信息,不参与哈希,避免宿主重启导致哈希漂移 |
| | | string execMode = HasColumn(deviceDt, "ExecutionMode") ? row["ExecutionMode"].ToString() : "Local"; |
| | | string authId = HasColumn(deviceDt, "AuthorityId") ? row["AuthorityId"].ToString() : ""; |
| | | string bkId = HasColumn(deviceDt, "BrokerId") ? row["BrokerId"].ToString() : ""; |
| | | string sessKey = HasColumn(deviceDt, "PhysicalSessionKey") ? row["PhysicalSessionKey"].ToString() : ""; |
| | | sb.Append($"DEVICE:{row["DeviceId"]}|{row["Kind"]}|{row["DriverType"]}|{row["Transport"]}|{row["Endpoint"]}|{execMode}|{authId}|{bkId}|{sessKey};"); |
| | | } |
| | | |
| | | var channelDt = helper.GetDataTable("SELECT * FROM DeviceChannel ORDER BY DeviceId, ChannelId;"); |
| | |
| | | |
| | | #endregion |
| | | |
| | | #region 发布门禁(V3.0 §05) |
| | | |
| | | /// <summary> |
| | | /// 发布前校验执行位置绑定:仲裁域必填且合法、Broker 域一致性、会话唯一宿主。 |
| | | /// 返回错误列表,为空则允许发布。 |
| | | /// </summary> |
| | | public List<string> ValidatePublishGate() |
| | | { |
| | | var errors = new List<string>(); |
| | | try |
| | | { |
| | | var devices = GetAllDevices(); |
| | | var authorityCatalog = V3AuthorityCatalog.AuthorityIds; |
| | | |
| | | // 1. 逐设备校验执行位置字段 |
| | | foreach (var dev in devices) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(dev.AuthorityId)) |
| | | { |
| | | errors.Add($"设备 [{dev.DeviceId}] 未选择仲裁域(AuthorityId 必填,旧数据请重新保存)"); |
| | | continue; |
| | | } |
| | | if (!authorityCatalog.Contains(dev.AuthorityId, StringComparer.OrdinalIgnoreCase)) |
| | | { |
| | | errors.Add($"设备 [{dev.DeviceId}] 仲裁域 [{dev.AuthorityId}] 不在配置枚举候选中"); |
| | | continue; |
| | | } |
| | | |
| | | if (dev.ExecutionMode == ExecutionModeKind.Local) |
| | | { |
| | | // Local 设备必须由本机 Local 域仲裁(已分配给唯一执行进程) |
| | | if (!V3AuthorityCatalog.IsLocalAuthority(dev.AuthorityId)) |
| | | { |
| | | errors.Add($"设备 [{dev.DeviceId}] 为 Local 直连,仲裁域应为 local- 前缀,实际为 [{dev.AuthorityId}]"); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | if (string.IsNullOrWhiteSpace(dev.BrokerId)) |
| | | { |
| | | errors.Add($"设备 [{dev.DeviceId}] 为 Broker 共享,必须指定 BrokerId"); |
| | | } |
| | | else if (!V3AuthorityCatalog.BrokerIds.Contains(dev.BrokerId, StringComparer.OrdinalIgnoreCase)) |
| | | { |
| | | errors.Add($"设备 [{dev.DeviceId}] BrokerId [{dev.BrokerId}] 不在配置枚举候选中"); |
| | | } |
| | | // BrokerId 与设备域一致:Broker 设备的 AuthorityId 即其 Broker 域 |
| | | else if (!string.Equals(dev.BrokerId, dev.AuthorityId, StringComparison.OrdinalIgnoreCase)) |
| | | { |
| | | errors.Add($"设备 [{dev.DeviceId}] BrokerId [{dev.BrokerId}] 与仲裁域 [{dev.AuthorityId}] 不一致"); |
| | | } |
| | | if (string.IsNullOrWhiteSpace(dev.PhysicalSessionKey)) |
| | | { |
| | | errors.Add($"设备 [{dev.DeviceId}] 为 Broker 共享,必须指定物理会话键 PhysicalSessionKey"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 2. 同一物理会话只能有一个宿主:相同 SessionKey 的设备必须归属同一仲裁域 |
| | | var sessionGroups = devices |
| | | .Where(d => !string.IsNullOrWhiteSpace(d.PhysicalSessionKey)) |
| | | .GroupBy(d => d.PhysicalSessionKey, StringComparer.OrdinalIgnoreCase); |
| | | foreach (var grp in sessionGroups) |
| | | { |
| | | var domains = grp.Select(d => d.AuthorityId).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); |
| | | if (domains.Count > 1) |
| | | { |
| | | errors.Add($"物理会话 [{grp.Key}] 被多个仲裁域引用: {string.Join(", ", domains)}(同一会话只能有一个宿主)"); |
| | | } |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"发布门禁校验失败: {ex.Message}", ex); |
| | | } |
| | | return errors; |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region 辅助方法 |
| | | |
| | | /// <summary> |