diff --git a/DataBase/XP.db b/DataBase/XP.db new file mode 100644 index 0000000..cbad8ab Binary files /dev/null and b/DataBase/XP.db differ diff --git a/XplorePlane/Services/Cnc/CncExecutionService.cs b/XplorePlane/Services/Cnc/CncExecutionService.cs index 9fefa9f..7bbc3a6 100644 --- a/XplorePlane/Services/Cnc/CncExecutionService.cs +++ b/XplorePlane/Services/Cnc/CncExecutionService.cs @@ -362,7 +362,7 @@ namespace XplorePlane.Services.Cnc Height = resultImage.PixelHeight }); nodeResult.Status = InspectionNodeStatus.Succeeded; - _mainViewportService?.SetManualImage(resultImage, $"CNC Node: {inspectionNode.Name}"); + _mainViewportService?.SetCncResultImage(resultImage, $"CNC 节点结果:{inspectionNode.Name}"); } } catch (Exception ex) diff --git a/XplorePlane/Services/InspectionResults/InspectionResultStore.cs b/XplorePlane/Services/InspectionResults/InspectionResultStore.cs index f825299..2e61f60 100644 --- a/XplorePlane/Services/InspectionResults/InspectionResultStore.cs +++ b/XplorePlane/Services/InspectionResults/InspectionResultStore.cs @@ -642,6 +642,19 @@ WHERE run_id = @run_id"; }; } + // Schema migration: columns added after initial release that may be missing in existing databases. + private static readonly (string Table, string Column, string Definition)[] MigrationColumns = + [ + ("inspection_runs", "status", "TEXT NOT NULL DEFAULT 'pending'"), + ("inspection_runs", "workpiece_id", "TEXT NOT NULL DEFAULT ''"), + ("inspection_runs", "serial_number", "TEXT NOT NULL DEFAULT ''"), + ("inspection_runs", "source_image_path", "TEXT NOT NULL DEFAULT ''"), + ("inspection_runs", "result_root_path", "TEXT NOT NULL DEFAULT ''"), + ("inspection_runs", "node_count", "INTEGER NOT NULL DEFAULT 0"), + ("inspection_node_results", "status", "TEXT NOT NULL DEFAULT 'Pending'"), + ("inspection_node_results", "duration_ms", "INTEGER NOT NULL DEFAULT 0"), + ]; + private async Task EnsureInitializedAsync() { if (_initialized) @@ -650,6 +663,16 @@ WHERE run_id = @run_id"; } Directory.CreateDirectory(_baseDirectory); + + // Step 1: Apply column migrations BEFORE running CreateTableSql. + // CreateTableSql contains CREATE INDEX statements that reference columns (e.g. "status") + // which may not exist in databases created by older versions of the app. + // Those index statements will fail with "no such column" even though they are + // guarded by IF NOT EXISTS, because SQLite validates column references at parse time. + // Running ALTER TABLE ADD COLUMN first ensures the columns exist before the index SQL runs. + await ApplySchemaMigrationsAsync().ConfigureAwait(false); + + // Step 2: Create any tables / indexes that do not yet exist. var result = await _db.ExecuteNonQueryAsync(CreateTableSql).ConfigureAwait(false); if (!result.IsSuccess) { @@ -660,6 +683,49 @@ WHERE run_id = @run_id"; _initialized = true; } + private async Task ApplySchemaMigrationsAsync() + { + foreach (var (table, column, definition) in MigrationColumns) + { + // Check if the table exists at all. If not, CreateTableSql will create it with all + // columns, so there is nothing to migrate. + var (tableExistResult, tableCount) = await _db.ExecuteScalarAsync( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=@name", + new Dictionary { ["name"] = table }).ConfigureAwait(false); + + if (!tableExistResult.IsSuccess || tableCount == 0) + continue; + + // Check whether the column already exists. + // pragma_table_info is a table-valued function and does not accept bound parameters, + // so the table name is inlined. All values in MigrationColumns are internal constants. + // ExecuteScalarAsync uses ExecuteScalarAsync() directly and does not go through + // DataTable, so it is not affected by the dflt_value BLOB mapping issue. + var (colExistResult, colCount) = await _db.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM pragma_table_info('{table}') WHERE name='{column}'" + ).ConfigureAwait(false); + + if (!colExistResult.IsSuccess) + { + _logger.Warn("Schema migration: could not check column '{0}' in '{1}', skipping: {2}", + column, table, colExistResult.Message); + continue; + } + + if (colCount == 0) + { + var alterResult = await _db.ExecuteNonQueryAsync( + $"ALTER TABLE {table} ADD COLUMN {column} {definition}").ConfigureAwait(false); + + if (alterResult.IsSuccess) + _logger.Info("Schema migration: added column '{0}' to table '{1}'", column, table); + else + _logger.Warn("Schema migration: failed to add column '{0}' to '{1}': {2}", + column, table, alterResult.Message); + } + } + } + private async Task EnsureRunExistsAsync(Guid runId) { var (result, value) = await _db.ExecuteScalarAsync( diff --git a/XplorePlane/Services/MainViewport/IMainViewportService.cs b/XplorePlane/Services/MainViewport/IMainViewportService.cs index 54c212b..f1b7e33 100644 --- a/XplorePlane/Services/MainViewport/IMainViewportService.cs +++ b/XplorePlane/Services/MainViewport/IMainViewportService.cs @@ -28,5 +28,11 @@ namespace XplorePlane.Services.MainViewport /// CNC 开始运行时传入 true,结束时传入 false。 /// void SetCncRunning(bool isRunning); + + /// + /// 由 CNC 执行引擎内部调用,将节点结果图像推送到 viewport。 + /// 与 不同,此方法在 CNC 运行期间不会被阻断。 + /// + void SetCncResultImage(ImageSource image, string label); } } diff --git a/XplorePlane/Services/MainViewport/MainViewportService.cs b/XplorePlane/Services/MainViewport/MainViewportService.cs index 86be1fa..385ee60 100644 --- a/XplorePlane/Services/MainViewport/MainViewportService.cs +++ b/XplorePlane/Services/MainViewport/MainViewportService.cs @@ -186,6 +186,28 @@ namespace XplorePlane.Services.MainViewport RaiseStateChanged(); } + public void SetCncResultImage(ImageSource image, string label) + { + if (image == null) + return; + + var displayLabel = string.IsNullOrWhiteSpace(label) ? "CNC 节点结果" : label; + + lock (_syncRoot) + { + // Intentionally bypasses the _isCncRunning guard so the node result + // is visible in the viewport immediately after each node completes. + _latestManualImage = image; + _latestManualInfo = displayLabel; + _currentSourceMode = MainViewportSourceMode.ManualImage; + _currentDisplayImage = _latestManualImage; + _currentDisplayInfo = _latestManualInfo; + } + + _logger.Info("[图像链路] MainViewportService.SetCncResultImage:已推送 CNC 节点结果图像 {Label}", displayLabel); + RaiseStateChanged(); + } + public void SetManualImage(ImageSource image, string filePath) { if (image == null)