From 75b1ccaa6dff92a63e276e6a79232113e21030eb Mon Sep 17 00:00:00 2001 From: ZhangAo Date: Tue, 4 Aug 2026 17:31:27 +0800 Subject: [PATCH] 20260804173126 --- .../docs/refactor/功能树与模块配置重构计划.md | 4 +- code/fms/docs/refactor/模块管理功能设计.md | 4 +- code/fms/docs/refactor/阶段一模块管理.md | 4 +- code/fms/fms-api/.api-8088.err.log | 0 code/fms/fms-api/.api-8088.log | 65 + .../033_create_module_structure.sql | 56 +- .../migrations/034_module_runtime_views.sql | 18 +- code/fms/fms-api/config/migrations/README.md | 15 +- .../tools/ApplyModuleRuntimeViews.java | 302 +++++ .../fms-module-list/FmsModuleListPage.vue | 707 ++-------- .../fms-module-list/FmsModuleTable.vue | 56 +- .../utils/createEditableCellRenderer.js | 69 +- .../src/layouts/components/AppSidebar.vue | 4 +- .../src/module-runtime/actionRegistry.js | 37 + .../src/module-runtime/conditionBuilder.js | 21 + .../src/module-runtime/configLoader.js | 141 ++ .../fms-vue/src/module-runtime/constants.js | 36 + .../src/module-runtime/fieldRegistry.js | 119 ++ code/fms/fms-vue/src/module-runtime/index.js | 11 + .../src/module-runtime/runtimeResolver.js | 146 +++ .../src/module-runtime/saveRequestBuilder.js | 13 + .../src/module-runtime/useModuleDataSource.js | 132 ++ .../src/module-runtime/useModuleEditor.js | 289 ++++ .../src/module-runtime/useModuleRuntime.js | 126 ++ code/fms/fms-vue/src/stores/permissions.js | 18 + .../fms-vue/src/views/base/contact/index.vue | 2 +- .../src/views/base/othercompany/index.vue | 1 - .../src/views/base/otherdata/index.vue | 51 +- .../components/BusinessStructurePanel.vue | 1163 +++++++++++++++++ .../components/ModuleResourcePanel.vue | 141 +- .../composables/useFunctionStructure.js | 5 + .../composables/useModuleResources.js | 95 ++ .../composables/usePageComposition.js | 34 +- .../src/views/module-management/index.vue | 90 +- .../fms-vue/src/views/system/user/index.vue | 2 +- code/fms/前端元数据运行时重构计划.md | 262 ++++ 36 files changed, 3521 insertions(+), 718 deletions(-) create mode 100644 code/fms/fms-api/.api-8088.err.log create mode 100644 code/fms/fms-api/.api-8088.log create mode 100644 code/fms/fms-api/config/migrations/tools/ApplyModuleRuntimeViews.java create mode 100644 code/fms/fms-vue/src/module-runtime/actionRegistry.js create mode 100644 code/fms/fms-vue/src/module-runtime/conditionBuilder.js create mode 100644 code/fms/fms-vue/src/module-runtime/configLoader.js create mode 100644 code/fms/fms-vue/src/module-runtime/constants.js create mode 100644 code/fms/fms-vue/src/module-runtime/fieldRegistry.js create mode 100644 code/fms/fms-vue/src/module-runtime/index.js create mode 100644 code/fms/fms-vue/src/module-runtime/runtimeResolver.js create mode 100644 code/fms/fms-vue/src/module-runtime/saveRequestBuilder.js create mode 100644 code/fms/fms-vue/src/module-runtime/useModuleDataSource.js create mode 100644 code/fms/fms-vue/src/module-runtime/useModuleEditor.js create mode 100644 code/fms/fms-vue/src/module-runtime/useModuleRuntime.js create mode 100644 code/fms/fms-vue/src/views/module-management/components/BusinessStructurePanel.vue create mode 100644 code/fms/前端元数据运行时重构计划.md diff --git a/code/fms/docs/refactor/功能树与模块配置重构计划.md b/code/fms/docs/refactor/功能树与模块配置重构计划.md index 1093421d..2ce9f3c0 100644 --- a/code/fms/docs/refactor/功能树与模块配置重构计划.md +++ b/code/fms/docs/refactor/功能树与模块配置重构计划.md @@ -1,4 +1,6 @@ -# 功能树与模块配置重构计划 +# 功能树与模块配置重构计划(历史方案) + +> 本文记录旧功能树方案,不再作为实现依据。当前模型以根目录 `新数据库结构.md` 和 `前端元数据运行时重构计划.md` 为准。 ## 1. 目标 diff --git a/code/fms/docs/refactor/模块管理功能设计.md b/code/fms/docs/refactor/模块管理功能设计.md index 682fb2e6..1335024b 100644 --- a/code/fms/docs/refactor/模块管理功能设计.md +++ b/code/fms/docs/refactor/模块管理功能设计.md @@ -1,4 +1,6 @@ -# 模块管理功能设计 +# 模块管理功能设计(历史方案) + +> 本文记录旧模块模型,不再作为实现依据。当前模型以根目录 `新数据库结构.md` 和 `前端元数据运行时重构计划.md` 为准。 > 范围:模块管理这一功能本身的设计定义,覆盖 **模块、字段定义、菜单、多语言、权限** 五大能力。 > 定位:本文档描述「模块管理页面能配置什么、各能力如何分工」,**不描述**通用 `ModuleTable`/`ModuleForm` 的渲染实现,也不描述角色赋权页面(授权在另一处)。 diff --git a/code/fms/docs/refactor/阶段一模块管理.md b/code/fms/docs/refactor/阶段一模块管理.md index c56cb22c..fcc96e20 100644 --- a/code/fms/docs/refactor/阶段一模块管理.md +++ b/code/fms/docs/refactor/阶段一模块管理.md @@ -1,4 +1,6 @@ -# 阶段一:模块管理 +# 阶段一:模块管理(历史方案) + +> 本文记录早期方案,不再作为实现依据。当前模型以根目录 `新数据库结构.md` 和 `前端元数据运行时重构计划.md` 为准。 ## 目标 diff --git a/code/fms/fms-api/.api-8088.err.log b/code/fms/fms-api/.api-8088.err.log new file mode 100644 index 00000000..e69de29b diff --git a/code/fms/fms-api/.api-8088.log b/code/fms/fms-api/.api-8088.log new file mode 100644 index 00000000..2b60cff3 --- /dev/null +++ b/code/fms/fms-api/.api-8088.log @@ -0,0 +1,65 @@ +[INFO] Scanning for projects... +[INFO] +[INFO] -------------------------< cn.g3soft:fms-api >-------------------------- +[INFO] Building fms-api 0.0.1-SNAPSHOT +[INFO] from pom.xml +[INFO] --------------------------------[ jar ]--------------------------------- +[INFO] +[INFO] >>> spring-boot-maven-plugin:4.1.0:run (default-cli) > test-compile @ fms-api >>> +[INFO] +[INFO] --- maven-resources-plugin:3.5.0:resources (default-resources) @ fms-api --- +[INFO] Copying 1 resource from src\main\resources to target\classes +[INFO] Copying 0 resource from src\main\resources to target\classes +[INFO] +[INFO] --- maven-compiler-plugin:3.15.0:compile (default-compile) @ fms-api --- +[INFO] Nothing to compile - all classes are up to date. +[INFO] +[INFO] --- maven-resources-plugin:3.5.0:testResources (default-testResources) @ fms-api --- +[INFO] skip non existing resourceDirectory D:\workspace\code\fms\fms-api\src\test\resources +[INFO] +[INFO] --- maven-compiler-plugin:3.15.0:testCompile (default-testCompile) @ fms-api --- +[INFO] Nothing to compile - all classes are up to date. +[INFO] +[INFO] <<< spring-boot-maven-plugin:4.1.0:run (default-cli) < test-compile @ fms-api <<< +[INFO] +[INFO] +[INFO] --- spring-boot-maven-plugin:4.1.0:run (default-cli) @ fms-api --- +[INFO] Attaching agents: [] + + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + + :: Spring Boot :: (v4.1.0) + +2026-08-04T15:49:27.744+08:00 INFO 10960 --- [fms-api] [ restartedMain] cn.g3soft.fmsapi.FmsApiApplication : Starting FmsApiApplication using Java 21 with PID 10960 (D:\workspace\code\fms\fms-api\target\classes started by Administrator in D:\workspace\code\fms\fms-api) +2026-08-04T15:49:27.753+08:00 INFO 10960 --- [fms-api] [ restartedMain] cn.g3soft.fmsapi.FmsApiApplication : No active profile set, falling back to 1 default profile: "default" +2026-08-04T15:49:27.901+08:00 INFO 10960 --- [fms-api] [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable +2026-08-04T15:49:27.902+08:00 INFO 10960 --- [fms-api] [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG' +2026-08-04T15:49:29.777+08:00 INFO 10960 --- [fms-api] [ restartedMain] o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port 8088 (http) +2026-08-04T15:49:29.805+08:00 INFO 10960 --- [fms-api] [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat] +2026-08-04T15:49:29.807+08:00 INFO 10960 --- [fms-api] [ restartedMain] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/11.0.22] +2026-08-04T15:49:29.858+08:00 INFO 10960 --- [fms-api] [ restartedMain] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 1954 ms +2026-08-04T15:49:30.455+08:00 INFO 10960 --- [fms-api] [ restartedMain] c.g3soft.fmsapi.utils.IpRegionSearcher : ip2region Searcher 初始化完成,数据大小 11114380 字节 +2026-08-04T15:49:31.639+08:00 INFO 10960 --- [fms-api] [ restartedMain] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 8088 (http) with context path '/api' +2026-08-04T15:49:31.656+08:00 INFO 10960 --- [fms-api] [ restartedMain] cn.g3soft.fmsapi.FmsApiApplication : Started FmsApiApplication in 5.07 seconds (process running for 7.639) +2026-08-04T15:50:46.035+08:00 INFO 10960 --- [fms-api] [nio-8088-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/api] : Initializing Spring DispatcherServlet 'dispatcherServlet' +2026-08-04T15:50:46.038+08:00 INFO 10960 --- [fms-api] [nio-8088-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' +2026-08-04T15:50:46.039+08:00 INFO 10960 --- [fms-api] [nio-8088-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms +2026-08-04T15:50:46.425+08:00 INFO 10960 --- [fms-api] [nio-8088-exec-1] com.alibaba.druid.pool.DruidDataSource : {dataSource-1,G3HD} inited +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD FAILURE +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 01:37 h +[INFO] Finished at: 2026-08-04T17:27:10+08:00 +[INFO] ------------------------------------------------------------------------ +[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:4.1.0:run (default-cli) on project fms-api: Process terminated with exit code: -1 -> [Help 1] +[ERROR] +[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. +[ERROR] Re-run Maven using the -X switch to enable full debug logging. +[ERROR] +[ERROR] For more information about the errors and possible solutions, please read the following articles: +[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException diff --git a/code/fms/fms-api/config/migrations/033_create_module_structure.sql b/code/fms/fms-api/config/migrations/033_create_module_structure.sql index 18a989ac..f235fd32 100644 --- a/code/fms/fms-api/config/migrations/033_create_module_structure.sql +++ b/code/fms/fms-api/config/migrations/033_create_module_structure.sql @@ -11,22 +11,55 @@ SET XACT_ABORT ON; IF DB_NAME() <> N'FMS' THROW 53300, N'This migration can only run against the FMS database.', 1; +/* CUTOVER_INSTALLED_GUARD_START */ IF OBJECT_ID(N'dbo.s_module_page', N'U') IS NOT NULL BEGIN PRINT N'Module architecture already installed.'; RETURN; END; +/* CUTOVER_INSTALLED_GUARD_END */ IF OBJECT_ID(N'dbo.s_module', N'U') IS NULL THROW 53301, N'dbo.s_module does not exist.', 1; -IF OBJECT_ID(N'dbo.s_module_profile', N'U') IS NOT NULL - OR OBJECT_ID(N'dbo.s_function_node', N'U') IS NOT NULL - OR OBJECT_ID(N'dbo.s_page_node', N'U') IS NOT NULL - THROW 53302, N'Partial module architecture detected. Restore the database before retrying.', 1; +DECLARE @HasPartialArchitecture bit = CASE WHEN + OBJECT_ID(N'dbo.s_module_profile', N'U') IS NOT NULL + OR OBJECT_ID(N'dbo.s_function_node', N'U') IS NOT NULL + OR OBJECT_ID(N'dbo.s_page_node', N'U') IS NOT NULL + THEN 1 ELSE 0 END; + +IF @HasPartialArchitecture = 1 AND ( + COL_LENGTH(N'dbo.s_module', N'b_parent_id') IS NULL + OR OBJECT_ID(N'dbo.s_module_field_list', N'U') IS NULL + OR OBJECT_ID(N'dbo.s_module_field_edit', N'U') IS NULL + OR OBJECT_ID(N'dbo.s_module_field_query', N'U') IS NULL + OR OBJECT_ID(N'dbo.s_module_field_group', N'U') IS NULL + OR OBJECT_ID(N'dbo.s_module_power', N'U') IS NULL + OR OBJECT_ID(N'dbo.s_module_field', N'U') IS NULL + OR OBJECT_ID(N'dbo.b_user_power', N'U') IS NULL + OR OBJECT_ID(N'dbo.b_user_module', N'U') IS NULL +) + THROW 53302, N'Unsafe partial module architecture detected. Restore the database before retrying.', 1; BEGIN TRANSACTION; + /* + Earlier implementation work may have created these three derived tables while + the original module tables were still authoritative. They are regenerated below + from the validated source model, so remove them inside this transaction only. + */ + IF @HasPartialArchitecture = 1 + BEGIN + IF OBJECT_ID(N'dbo.s_page_node', N'U') IS NOT NULL DROP TABLE dbo.s_page_node; + IF OBJECT_ID(N'dbo.s_function_node', N'U') IS NOT NULL DROP TABLE dbo.s_function_node; + IF OBJECT_ID(N'dbo.s_module_profile', N'U') IS NOT NULL DROP TABLE dbo.s_module_profile; + END; +GO + +/* RETURN in the first batch means the architecture was already installed. */ +IF @@TRANCOUNT = 0 + RETURN; + /* Preserve the source tables only for the duration of this transaction. */ DECLARE @LegacyTables TABLE (b_old sysname NOT NULL, b_legacy sysname NOT NULL, b_xh int NOT NULL); INSERT INTO @LegacyTables VALUES @@ -50,14 +83,16 @@ BEGIN TRANSACTION; DECLARE @OldName sysname; DECLARE @LegacyName sysname; + DECLARE @QualifiedOldName nvarchar(517); DECLARE table_cursor CURSOR LOCAL FAST_FORWARD FOR SELECT b_old, b_legacy FROM @LegacyTables ORDER BY b_xh; OPEN table_cursor; FETCH NEXT FROM table_cursor INTO @OldName, @LegacyName; WHILE @@FETCH_STATUS = 0 BEGIN - IF OBJECT_ID(N'dbo.' + @OldName, N'U') IS NOT NULL - EXEC sys.sp_rename N'dbo.' + @OldName, @LegacyName, N'OBJECT'; + SET @QualifiedOldName = QUOTENAME(N'dbo') + N'.' + QUOTENAME(@OldName); + IF OBJECT_ID(@QualifiedOldName, N'U') IS NOT NULL + EXEC sys.sp_rename @QualifiedOldName, @LegacyName, N'OBJECT'; FETCH NEXT FROM table_cursor INTO @OldName, @LegacyName; END; CLOSE table_cursor; @@ -66,6 +101,7 @@ BEGIN TRANSACTION; /* Constraint names are schema-wide and sp_rename does not change them with the table. */ DECLARE @ConstraintName sysname; DECLARE @LegacyConstraintName sysname; + DECLARE @QualifiedConstraintName nvarchar(517); DECLARE constraint_cursor CURSOR LOCAL FAST_FORWARD FOR SELECT object_row.name, CONVERT(sysname, N'z_legacy_c_' + CONVERT(varchar(20), object_row.object_id)) @@ -78,7 +114,8 @@ BEGIN TRANSACTION; FETCH NEXT FROM constraint_cursor INTO @ConstraintName, @LegacyConstraintName; WHILE @@FETCH_STATUS = 0 BEGIN - EXEC sys.sp_rename N'dbo.' + QUOTENAME(@ConstraintName), @LegacyConstraintName, N'OBJECT'; + SET @QualifiedConstraintName = QUOTENAME(N'dbo') + N'.' + QUOTENAME(@ConstraintName); + EXEC sys.sp_rename @QualifiedConstraintName, @LegacyConstraintName, N'OBJECT'; FETCH NEXT FROM constraint_cursor INTO @ConstraintName, @LegacyConstraintName; END; CLOSE constraint_cursor; @@ -798,9 +835,10 @@ BEGIN TRANSACTION; DECLARE @AdminId bigint; SELECT TOP (1) @AdminId = b_id FROM dbo.b_user - WHERE b_account = N'g3soft' AND b_canuse = 1 ORDER BY b_id; + WHERE b_canuse = 1 + ORDER BY CASE WHEN b_account = N'g3soft' THEN 0 ELSE 1 END, b_id; IF @AdminId IS NULL - THROW 53305, N'Enabled g3soft administrator not found.', 1; + THROW 53305, N'Enabled administrator user not found.', 1; INSERT INTO dbo.b_role (b_id, b_code, b_name, b_i18n, b_is_super, b_canuse, b_xh) VALUES (@AdminId, 'system_admin', N'系统管理员', 'meta.role.' + CONVERT(varchar(30), @AdminId), 1, 1, 0); diff --git a/code/fms/fms-api/config/migrations/034_module_runtime_views.sql b/code/fms/fms-api/config/migrations/034_module_runtime_views.sql index 45d4d5ec..3089f5b8 100644 --- a/code/fms/fms-api/config/migrations/034_module_runtime_views.sql +++ b/code/fms/fms-api/config/migrations/034_module_runtime_views.sql @@ -2,8 +2,22 @@ SET NOCOUNT ON; SET XACT_ABORT ON; -IF OBJECT_ID(N'dbo.s_module', N'U') IS NULL - THROW 53400, N'Module architecture is not installed.', 1; +IF EXISTS ( + SELECT required.name + FROM (VALUES + (N's_module'), (N's_module_page'), (N's_module_data'), (N's_module_report'), + (N's_module_dashboard'), (N's_module_workflow'), (N's_function_node'), + (N's_page_node'), (N's_module_profile'), (N's_module_field'), + (N's_module_field_list'), (N's_module_field_form'), (N's_module_field_query'), + (N's_module_action'), (N'b_role'), (N'b_user_role'), + (N'b_role_function_node'), (N'b_role_page_node'), (N'b_role_action'), + (N'b_user_function_node'), (N'b_user_page_node'), (N'b_user_action') + ) AS required(name) + LEFT JOIN sys.tables AS actual + ON actual.name = required.name AND actual.schema_id = SCHEMA_ID(N'dbo') + WHERE actual.object_id IS NULL +) + THROW 53400, N'Module architecture is incomplete. Run migration 033 first.', 1; GO CREATE OR ALTER VIEW dbo.v_s_function_node diff --git a/code/fms/fms-api/config/migrations/README.md b/code/fms/fms-api/config/migrations/README.md index 813e83a0..cf53830c 100644 --- a/code/fms/fms-api/config/migrations/README.md +++ b/code/fms/fms-api/config/migrations/README.md @@ -9,7 +9,20 @@ Run against a backed-up `FMS` database in this order: 3. `verify_module_architecture.sql` `033` is a one-time transaction: it migrates valid metadata and removes the old metadata tables only after conversion checks pass. It intentionally does not create compatibility views or dual writes. +If the earlier intermediate implementation created only `s_module_profile`, `s_function_node`, or `s_page_node`, `033` verifies that the complete source model still exists and regenerates those derived tables inside the same transaction. Other partial target states are rejected. -`034` uses SQL Server `GO` batch separators because each `CREATE OR ALTER VIEW/TRIGGER` must start its batch. Use SSMS, Azure Data Studio, `sqlcmd`, or another runner that understands `GO`. +`033` and `034` use SQL Server `GO` batch separators. Use SSMS, Azure Data Studio, `sqlcmd`, or another runner that understands `GO`. `verify_module_architecture.sql` is a target-schema verification script. It does not modify application tables. + +To check and conditionally apply `034` with the repository JDBC driver on Windows: + +```powershell +$jdk = "D:\devtool\jdk\jdk-21\bin" +$driver = "tools\migration\mssql-jdbc-13.4.0.jre11.jar" +& "$jdk\javac.exe" -encoding UTF-8 -cp $driver -d $env:TEMP config\migrations\tools\ApplyModuleRuntimeViews.java +& "$jdk\java.exe" -cp "$env:TEMP;$driver" ApplyModuleRuntimeViews +``` + +The tool refuses non-`FMS` catalogs and executes `034` only when a required runtime view is missing. +For a database in the validated intermediate state, run `ApplyModuleRuntimeViews --dry-run-cutover` first. After verifying a database backup, `ApplyModuleRuntimeViews --apply-cutover` executes `033`, then conditionally executes `034` and verifies all required runtime views. diff --git a/code/fms/fms-api/config/migrations/tools/ApplyModuleRuntimeViews.java b/code/fms/fms-api/config/migrations/tools/ApplyModuleRuntimeViews.java new file mode 100644 index 00000000..39d82002 --- /dev/null +++ b/code/fms/fms-api/config/migrations/tools/ApplyModuleRuntimeViews.java @@ -0,0 +1,302 @@ +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.HashSet; + +/** Applies the module cutover and runtime views with prerequisite and completion checks. */ +public final class ApplyModuleRuntimeViews { + private static final List REQUIRED_TABLES = List.of( + "s_module", + "s_module_page", + "s_module_data", + "s_module_report", + "s_module_dashboard", + "s_module_workflow", + "s_function_node", + "s_page_node", + "s_module_profile", + "s_module_field", + "s_module_field_list", + "s_module_field_form", + "s_module_field_query", + "s_module_action", + "b_role", + "b_user_role", + "b_role_function_node", + "b_role_page_node", + "b_role_action", + "b_user_function_node", + "b_user_page_node", + "b_user_action" + ); + private static final List REQUIRED_VIEWS = List.of( + "v_s_function_node", + "v_s_page_node", + "v_s_module_profile", + "v_s_module_runtime", + "v_s_module_field_list", + "v_s_module_field_form", + "v_s_module_field_query", + "v_s_module_action", + "v_s_user_function_permission", + "v_s_user_page_permission", + "v_s_user_action_permission" + ); + private static final List INSPECTION_TABLES = List.of( + "s_module", + "s_module_profile", + "s_function_node", + "s_page_node", + "s_module_field", + "s_module_field_list", + "s_module_field_edit", + "s_module_field_query", + "s_module_field_group", + "s_module_power", + "b_user_module", + "b_user_power" + ); + public static void main(String[] args) throws Exception { + Path apiRoot = Path.of("").toAbsolutePath().normalize(); + boolean hasModeArgument = args.length > 0 && args[0].startsWith("--"); + String mode = hasModeArgument ? args[0] : "--views-only"; + int configIndex = hasModeArgument ? 1 : 0; + Path configPath = args.length > configIndex + ? apiRoot.resolve(args[configIndex]).normalize() + : apiRoot.resolve("config/dbconfigs/g3hd.properties"); + Path cutoverPath = apiRoot.resolve("config/migrations/033_create_module_structure.sql"); + Path migrationPath = apiRoot.resolve("config/migrations/034_module_runtime_views.sql"); + requirePathWithin(apiRoot, configPath); + requirePathWithin(apiRoot, cutoverPath); + requirePathWithin(apiRoot, migrationPath); + + Properties properties = new Properties(); + try (InputStream input = Files.newInputStream(configPath)) { + properties.load(input); + } + + try (Connection connection = DriverManager.getConnection( + required(properties, "url"), + required(properties, "username"), + required(properties, "password") + )) { + if (!"FMS".equalsIgnoreCase(connection.getCatalog())) { + throw new IllegalStateException("Refusing to migrate database: " + connection.getCatalog()); + } + + if (mode.equals("--dry-run-cutover")) { + List cutoverTables = missingObjects(connection, "sys.tables", REQUIRED_TABLES); + if (!cutoverTables.isEmpty()) { + executeCutover(connection, cutoverPath, true); + } + System.out.println("PASS|033_create_module_structure.sql|dry_run=true|committed=false"); + return; + } + if (mode.equals("--apply-cutover")) { + List cutoverTables = missingObjects(connection, "sys.tables", REQUIRED_TABLES); + if (cutoverTables.isEmpty()) { + System.out.println("PASS|033_create_module_structure.sql|applied=false"); + } else { + executeCutover(connection, cutoverPath, false); + cutoverTables = missingObjects(connection, "sys.tables", REQUIRED_TABLES); + if (!cutoverTables.isEmpty()) { + throw new IllegalStateException( + "Migration 033 did not complete; tables remain missing: " + + String.join(",", cutoverTables) + ); + } + System.out.println("APPLIED|033_create_module_structure.sql"); + } + } else if (!mode.equals("--views-only")) { + throw new IllegalArgumentException("Unknown mode: " + mode); + } + + List missingTables = missingObjects(connection, "sys.tables", REQUIRED_TABLES); + if (!missingTables.isEmpty()) { + System.out.println("MISSING_PREREQUISITE_TABLES|" + String.join(",", missingTables)); + printPartialState(connection); + throw new IllegalStateException("Run migration 033 before migration 034"); + } + + List missingBefore = missingViews(connection); + if (missingBefore.isEmpty()) { + System.out.println("PASS|required_module_views=present|applied=false"); + return; + } + System.out.println("MISSING|" + String.join(",", missingBefore)); + + connection.setAutoCommit(false); + try { + String migration = Files.readString(migrationPath, StandardCharsets.UTF_8); + int batches = executeBatches(connection, migration); + List missingAfter = missingViews(connection); + if (!missingAfter.isEmpty()) { + throw new IllegalStateException( + "Required views remain missing: " + String.join(",", missingAfter) + ); + } + connection.commit(); + System.out.println("APPLIED|034_module_runtime_views.sql|batches=" + batches); + System.out.println("PASS|required_module_views=present|applied=true"); + } catch (Exception exception) { + connection.rollback(); + throw exception; + } finally { + connection.setAutoCommit(true); + } + } + } + + private static List missingViews(Connection connection) throws Exception { + return missingObjects(connection, "sys.views", REQUIRED_VIEWS); + } + + private static List missingObjects( + Connection connection, + String catalogView, + List requiredObjects + ) throws Exception { + Set existing = new HashSet<>(); + try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery( + "SELECT name FROM " + catalogView + " WHERE schema_id = SCHEMA_ID(N'dbo')" + )) { + while (rows.next()) existing.add(rows.getString(1)); + } + List missing = new ArrayList<>(); + for (String required : requiredObjects) { + if (!existing.contains(required)) missing.add(required); + } + return missing; + } + + private static int executeBatches(Connection connection, String migration) throws Exception { + String[] batches = migration.split("(?im)^\\s*GO\\s*(?:--.*)?$"); + int executed = 0; + for (String batch : batches) { + if (batch.isBlank()) continue; + try (Statement statement = connection.createStatement()) { + boolean hasResult = statement.execute(batch); + while (true) { + if (hasResult) { + try (ResultSet rows = statement.getResultSet()) { + while (rows.next()) { + // Drain server results so the complete SQL batch finishes. + } + } + } else if (statement.getUpdateCount() == -1) { + break; + } + hasResult = statement.getMoreResults(); + } + } + executed++; + } + return executed; + } + + private static void executeCutover(Connection connection, Path migrationPath, boolean dryRun) + throws Exception { + connection.setAutoCommit(false); + String migration = Files.readString(migrationPath, StandardCharsets.UTF_8); + migration = migration.replaceAll( + "(?s)/\\* CUTOVER_INSTALLED_GUARD_START \\*/.*?" + + "/\\* CUTOVER_INSTALLED_GUARD_END \\*/", + "" + ); + migration = migration.replaceAll( + "(?s)/\\* RETURN in the first batch means the architecture was already installed\\. \\*/" + + "\\s*IF @@TRANCOUNT = 0\\s*RETURN;", + "" + ); + migration = migration.replaceAll("(?im)^\\s*BEGIN TRANSACTION;\\s*$", ""); + migration = migration.replaceAll("(?im)^\\s*COMMIT TRANSACTION;\\s*$", ""); + try { + printCutoverState(connection, "before"); + int batches = executeBatches(connection, migration); + printCutoverState(connection, "after-sql"); + List missingTables = missingObjects(connection, "sys.tables", REQUIRED_TABLES); + if (!missingTables.isEmpty()) { + throw new IllegalStateException( + "Migration 033 did not create required tables: " + String.join(",", missingTables) + ); + } + if (dryRun) { + connection.rollback(); + } else { + connection.commit(); + } + printCutoverState(connection, "after-jdbc"); + System.out.println("CUTOVER_SQL|dry_run=" + dryRun + "|batches=" + batches); + } catch (Exception exception) { + try (Statement statement = connection.createStatement()) { + statement.execute("IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION"); + } + connection.rollback(); + throw exception; + } finally { + connection.setAutoCommit(true); + } + } + + private static void printCutoverState(Connection connection, String phase) throws Exception { + try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery( + "SELECT @@TRANCOUNT, CASE WHEN OBJECT_ID(N'dbo.s_module_page', N'U') IS NULL THEN 0 ELSE 1 END" + )) { + rows.next(); + System.out.println("CUTOVER_STATE|" + phase + "|trancount=" + rows.getInt(1) + + "|s_module_page=" + (rows.getInt(2) == 1)); + } + } + + + private static void printPartialState(Connection connection) throws Exception { + Set existing = new HashSet<>(); + try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery( + "SELECT name FROM sys.tables WHERE schema_id = SCHEMA_ID(N'dbo')" + )) { + while (rows.next()) existing.add(rows.getString(1)); + } + for (String table : INSPECTION_TABLES) { + if (!existing.contains(table)) continue; + try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery( + "SELECT COUNT_BIG(*) FROM dbo.[" + table.replace("]", "]]" ) + "]" + )) { + rows.next(); + System.out.println("TABLE_STATE|" + table + "|rows=" + rows.getLong(1)); + } + List columns = new ArrayList<>(); + try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery( + "SELECT column_row.name, TYPE_NAME(column_row.user_type_id), column_row.is_nullable " + + "FROM sys.columns AS column_row " + + "WHERE column_row.object_id = OBJECT_ID(N'dbo.[" + + table.replace("]", "]]" ) + + "]') ORDER BY column_row.column_id" + )) { + while (rows.next()) { + columns.add(rows.getString(1) + ":" + rows.getString(2) + + (rows.getBoolean(3) ? "?" : "")); + } + } + System.out.println("TABLE_COLUMNS|" + table + "|" + String.join(",", columns)); + } + } + + private static String required(Properties properties, String key) { + String value = properties.getProperty(key); + if (value == null || value.isBlank()) throw new IllegalStateException("Missing property: " + key); + return value.trim(); + } + + private static void requirePathWithin(Path root, Path path) { + if (!path.startsWith(root)) throw new IllegalArgumentException("Path is outside API root: " + path); + } +} diff --git a/code/fms/fms-vue/src/components/fms-module-list/FmsModuleListPage.vue b/code/fms/fms-vue/src/components/fms-module-list/FmsModuleListPage.vue index 7e50df2b..581c3d76 100644 --- a/code/fms/fms-vue/src/components/fms-module-list/FmsModuleListPage.vue +++ b/code/fms/fms-vue/src/components/fms-module-list/FmsModuleListPage.vue @@ -1,40 +1,27 @@ diff --git a/code/fms/fms-vue/src/components/fms-module-list/FmsModuleTable.vue b/code/fms/fms-vue/src/components/fms-module-list/FmsModuleTable.vue index a1d8a48a..21596729 100644 --- a/code/fms/fms-vue/src/components/fms-module-list/FmsModuleTable.vue +++ b/code/fms/fms-vue/src/components/fms-module-list/FmsModuleTable.vue @@ -1,9 +1,9 @@ + + + + diff --git a/code/fms/fms-vue/src/views/module-management/components/ModuleResourcePanel.vue b/code/fms/fms-vue/src/views/module-management/components/ModuleResourcePanel.vue index 6c819fef..92a04e4f 100644 --- a/code/fms/fms-vue/src/views/module-management/components/ModuleResourcePanel.vue +++ b/code/fms/fms-vue/src/views/module-management/components/ModuleResourcePanel.vue @@ -1,7 +1,9 @@