diff --git a/code/fms/FMS元数据驱动架构重构方案.md b/code/fms/FMS元数据驱动架构重构方案.md index e5fbb627..a4d1f46e 100644 --- a/code/fms/FMS元数据驱动架构重构方案.md +++ b/code/fms/FMS元数据驱动架构重构方案.md @@ -7,17 +7,17 @@ ## 1. 结论摘要 -本次重构保留并强化**元数据驱动**这一核心概念。模块定义、字段语义、关系、页面视图和通用行为仍由元数据描述,通用运行时读取已发布的元数据运行。 +本次重构保留并强化**元数据驱动**这一核心概念。模块定义、字段语义、关系、页面视图和通用行为由元数据描述,通用运行时读取已发布的元数据运行;列表、查询、保存、权限、关系、工作流和公式等能力统一通过通用接口执行,复杂逻辑由 SQL、函数库和业务处理器补充。 项目属于草稿 / Demo 阶段,不要求兼容现有元数据表结构和配置 JSON。可以重做元数据数据库,并整体替换配置和运行时主链路;不需要新旧格式长期双写或并行维护。 ```text 物理数据源与表结构 → 模块元数据 → 模板 / 默认值 → 校验 / 编译 / 发布 ↓ - 统一模块运行时 → SQL 业务表 / 视图 + 统一模块运行时 + 通用代码协议 → SQL 业务表 / 视图 ``` -SQL Server 中的真实业务表和视图继续作为业务数据事实来源。元数据描述如何理解、呈现和操作这些数据;不采用 EAV 存储所有业务记录,也不让元数据引擎任意生成业务表。 +SQL Server 中的真实业务表和视图继续作为业务数据事实来源。元数据描述如何理解、呈现和操作这些数据,通用 SQL、函数库和业务处理器协议可以补充复杂逻辑;不采用 EAV 存储所有业务记录,也不让元数据引擎任意生成业务表。 ## 2. 重构目标与边界 @@ -27,9 +27,10 @@ SQL Server 中的真实业务表和视图继续作为业务数据事实来源。 - 默认配置可推导、可重复生成;用户明确调整过的配置不被覆盖。 - 元数据发布前经过统一校验,运行时消费同一份有效定义。 - 元数据数据库可以整体重做,不受 `s_module`、`s_field`、`s_module_schema` 现有结构约束。 -- 复杂查询和业务动作保留 SQL / 后端扩展出口,不强迫所有逻辑都配置化。 +- 列表、查询、保存、权限、关系、工作流和公式均提供元数据默认实现,并保留统一 SQL / 函数库 / 业务处理器协议。 +- 后端通过通用接口绑定 SQL、函数和处理器,不为具体模块增加特殊 API,也不在元数据中保存可执行代码。 - 不将所有字段值改为 EAV 或通用 JSON;业务表仍通过 SQL migration / 数据库变更管理。 -- 不建设可执行任意脚本的规则引擎,不把库存、财务、结算等复杂事务塞进配置。 +- 不建设可执行任意脚本的规则引擎,不把库存、财务、结算等复杂事务强行塞进配置;复杂逻辑通过受控的代码扩展实现。 - 不为本次目标无差别重写认证、文件、导出等稳定模块,只调整必要集成点。 - 不要求兼容旧元数据格式;需要保留的 Demo 数据可一次性导入,否则重建 / 播种。 @@ -48,7 +49,7 @@ SQL Server 中的真实业务表和视图继续作为业务数据事实来源。 ↓ SQL Server 表、视图与只读查询 SQL ↓ - 注册的后端业务动作及工作流扩展 + 统一模块接口、SQL、函数库及业务处理器 ``` ### 配置面 @@ -57,11 +58,31 @@ SQL Server 中的真实业务表和视图继续作为业务数据事实来源。 ### 元数据编译与校验 -后端维护权威校验与解析逻辑。发布时将模块定义、模板默认值和用户覆盖项合成为有效定义,并检查字段引用、数据源、视图、关系、查询条件和动作引用。前端预览使用后端返回的有效定义,避免前后端分别推导默认规则。 +后端维护权威校验与解析逻辑。发布时将模块定义、模板默认值和用户覆盖项合成为有效定义,并检查字段引用、数据源、视图、关系和查询条件。前端预览使用后端返回的有效定义,避免前后端分别推导默认规则。 ### 运行面 -通用运行时负责适合通用化的列表、过滤、排序、分页、字段呈现、标准保存和权限约束。查询值参数化;自定义查询入口仅用于只读查询。多表事务、库存过账、财务结算等动作由后端注册的业务处理器执行。元数据声明动作入口和参数契约,不保存可执行代码。 +后端只提供统一的模块接口。统一接口读取已发布的 `ModuleDefinition`,负责列表、查询、保存、权限、关系、工作流和公式等通用能力;查询值参数化,保存和动作在统一事务上下文中执行。SQL、公式函数和业务代码作为通用接口内部的实现来源,不为每个模块新增专用 API。 + +### 通用接口与代码控制 + +每项能力都遵循同一套调用方式: + +1. 元数据提供模块、字段、视图、关系和默认规则。 +2. 通用接口根据模块定义执行默认列表、查询、保存、权限、关系、工作流和公式逻辑。 +3. 需要代码控制时,在通用接口内部调用统一的 SQL、函数或业务处理器协议;调用方式不随模块变化。 + +| 能力 | 元数据默认职责 | 代码扩展或接管职责 | +|---|---|---| +| 列表 | 数据源、字段、过滤、排序、分页和列展示 | 自定义查询、聚合、行转换和复杂数据范围 | +| 查询 | 条件字段、操作符、条件组和参数定义 | 特殊条件解析、跨表查询和只读 SQL | +| 保存 | 标准增删改、字段校验、主键和关系处理 | 事务编排、状态流转、库存扣减和保存前后钩子 | +| 权限 | 模块、字段、角色和数据范围声明 | 上下文权限判断、动态授权和特殊拒绝规则 | +| 关系 | 主从、引用、多对多和加载规则 | 复杂关联加载、级联校验和同步逻辑 | +| 工作流 | 节点、连线、参与者和状态定义 | 审批条件、业务动作、会签规则和外部调用 | +| 公式 | 表达式、字段依赖和受限通用函数 | 通用函数库、复杂计算、跨单据取数和异步计算 | + +代码实现通过后端统一注册表或 SQL 目录管理,不在数据库中保存 Java 类名、脚本或任意可执行内容。通用权限约束不能被业务代码绕过;业务代码可以补充通用接口的规则。模块只声明需要的标准能力和参数,不声明专用接口。 ## 4. 元数据逻辑模型 @@ -69,7 +90,7 @@ SQL Server 中的真实业务表和视图继续作为业务数据事实来源。 ### 模块 -包含稳定编码、名称、多语言键、类型、状态、组织层级、查询数据源、标准保存目标 / 主键、字段、关系、视图、动作引用、模板和发布版本。模块类型收敛为少数明确类别,如标准数据、只读查询、业务动作模块。 +包含稳定编码、名称、多语言键、类型、状态、组织层级、查询数据源、标准保存目标 / 主键、数据范围字段、字段、关系、视图、模板和发布版本。模块类型收敛为少数明确类别,如标准数据、只读查询、业务动作模块。 ### 字段 @@ -87,7 +108,7 @@ SQL Server 中的真实业务表和视图继续作为业务数据事实来源。 - 表单、列表、查询、详情是不同视图类型,只保存顺序、分组、布局、列宽、排序、操作符等视图特有信息;布局采用可读、可校验的结构化定义,不保存画布坐标。 - 字段参与视图的默认状态由字段能力、模板和默认策略推导;个性化差异作为显式覆盖。 - 简单校验和通用 CRUD 可由元数据表达;复杂业务动作映射到稳定的后端处理器编码。 -- 工作流、权限、公式等能力通过稳定引用接入,不重复定义模块字段。 +- 列表、查询、保存、权限、关系、工作流和公式通过统一模块接口接入;这些能力引用模块定义和已发布快照,不为每个模块建立特殊接口。 ## 5. 元数据数据库重构 @@ -95,31 +116,226 @@ SQL Server 中的真实业务表和视图继续作为业务数据事实来源。 | 逻辑实体 | 职责 | 建议存储 | |---|---|---| -| 模块 | 身份、类型、数据源、状态、组织关系 | 规范化关系表 | +| 模块 | 身份、类型、状态、树结构、数据源和通用运行参数 | 规范化关系表 | | 模块字段 | 字段编码、物理映射、业务语义、能力标记 | 规范化关系表;模块内编码唯一 | | 模块关系 | 关系类型及源 / 目标字段 | 规范化关系表;发布时校验引用 | -| 模块视图 | 表单、列表、查询、详情定义 | 按模块和视图类型区分;灵活布局可用 JSON | +| 模块视图 | 表单、列表、查询、详情及布局 | `s_module_schema`,按视图类型保存受校验 JSON | | 模板 | 默认字段能力、视图结构、推荐规则 | 可版本化定义 | | 发布版本 | 不可变快照、版本号、校验结果、发布时间 | 版本记录;运行时按已发布版本读取 | -| 动作注册 | 动作编码与后端处理器参数契约 | 只存稳定编码和契约,不存可执行代码 | + +### 推荐目标表结构 + +建议继续使用现有的 `s_` 前缀和核心表。后端是通用接口,因此不再为数据源、视图和代码扩展分别增加运行时表:模块数据源保留在 `s_module`,视图 JSON 保留在 `s_module_schema`,复杂代码由后端统一实现和注册。只新增发布版本和快照表解决草稿与运行态隔离。 + +| 目标实体 | 关键字段 / 组成 | 说明 | +|---|---|---| +| 模块 `s_module` | 模块编码、父级、名称、多语言键、模块类型、查询/保存来源、主键、数据范围和默认排序 | 保留当前模块主表,清理无约束的大 JSON;数据源由通用接口读取 | +| 模块字段 `s_field` | 模块、字段编码、来源类型、来源名称/别名、名称、多语言键、语义类型、字段能力、启用状态 | 支持物理列、查询别名、计算字段和虚拟字段;物理 SQL 类型仍优先从数据库读取 | +| 模块视图 `s_module_schema` | 模块、视图类型、结构版本、`b_schema_json` | JSON 保存分组、字段引用、列属性、查询条件、控件和公式;发布时统一校验 | +| 模块关系 `s_relation` | 关系编码、源/目标模块字段、关系类型、用途、基数、加载规则、删除策略 | 发布时校验模块和字段引用,保留现有删除语义 | +| 模块版本 `s_module_version` | 模块、版本号、草稿/已发布/废弃状态、校验摘要、发布人、发布时间 | 管理草稿与当前有效版本边界 | +| 发布快照 `s_module_snapshot` | 版本、编译后的 `ModuleDefinition`、快照摘要、生成时间 | 运行时只读取不可变快照,不直接读取编辑态表 | +| 模板及模板版本 `s_template` / `s_template_version` | 模板编码、版本、默认字段能力、视图 JSON、推荐规则 | 模板是可编辑初始值,不是硬编码页面 | +| 用户偏好 `s_user_module_pref` | 用户、模块、视图、相对默认值的覆盖 | 可选保留,只能改变布局偏好,不能改变语义、权限或数据源 | + +`s_module_schema.b_schema_json` 不是无约束 JSON:必须包含 `schemaVersion`,只能引用本模块已登记的字段和关系,并由后端 JSON Schema 和发布校验共同约束。编辑态以 `s_module_schema` 为事实来源,发布后编译成 `s_module_snapshot`。 + +### 推荐 DDL(示例) + +```sql +create table dbo.s_module ( + b_id varchar(80) not null primary key, + b_parent_id varchar(80) null, + b_depth int not null default 0, + b_path varchar(1500) not null, + b_name nvarchar(200) not null, + b_i18n varchar(150) null, + b_module_type varchar(20) not null, -- module / data / virtual + b_view_table varchar(256) null, + b_save_table varchar(256) null, + b_key_field varchar(80) null, + b_scope_field varchar(80) null, + b_order_sql varchar(500) null, + b_query_sql nvarchar(max) null, + b_config_json nvarchar(max) null, + b_canuse tinyint not null default 1, + b_xh int not null default 0, + b_bz nvarchar(2000) null, + b_updated_by varchar(50) null, + b_updated_at datetime2 null +); + +create table dbo.s_field ( + b_module_id varchar(80) not null, + b_field varchar(80) not null, + b_source_type varchar(20) not null, -- physical / alias / computed / virtual + b_source_name varchar(256) null, + b_name nvarchar(200) not null, + b_i18n varchar(150) null, + b_semantic_type varchar(40) not null, + b_can_read tinyint not null default 1, + b_can_edit tinyint not null default 0, + b_can_filter tinyint not null default 0, + b_can_sort tinyint not null default 0, + b_can_export tinyint not null default 0, + b_canuse tinyint not null default 1, + b_xh int not null default 0, + primary key (b_module_id, b_field) +); + +create table dbo.s_module_schema ( + b_module_id varchar(80) not null, + b_schema_type varchar(20) not null, -- view / edit / query / detail + b_schema_version int not null default 1, + b_schema_json nvarchar(max) not null, + b_canuse tinyint not null default 1, + b_updated_by varchar(50) null, + b_updated_at datetime2 null, + primary key (b_module_id, b_schema_type) +); + +create table dbo.s_relation ( + b_relation_code varchar(100) not null primary key, + b_source_module_id varchar(80) not null, + b_source_field varchar(80) not null, + b_target_module_id varchar(80) not null, + b_target_field varchar(80) not null, + b_relation_type varchar(30) not null, -- one_to_one / one_to_many / many_to_one + b_usage varchar(30) not null, -- reference / detail / selector / load + b_load_mode varchar(20) not null default 'lazy', + b_on_delete varchar(20) not null default 'restrict', + b_canuse tinyint not null default 1, + b_xh int not null default 0 +); + +create table dbo.s_module_version ( + b_id varchar(100) not null primary key, + b_module_id varchar(80) not null, + b_version_no int not null, + b_status varchar(20) not null, -- draft / published / retired + b_checksum varchar(128) null, + b_published_by varchar(50) null, + b_published_at datetime2 null, + b_created_by varchar(50) null, + b_created_at datetime2 not null default sysdatetime(), + unique (b_module_id, b_version_no) +); + +create table dbo.s_module_snapshot ( + b_version_id varchar(100) not null primary key, + b_module_id varchar(80) not null, + b_snapshot_json nvarchar(max) not null, + b_checksum varchar(128) not null, + b_created_at datetime2 not null default sysdatetime() +); + +create table dbo.s_user_module_pref ( + b_user_id varchar(50) not null, + b_module_id varchar(80) not null, + b_schema_type varchar(50) not null, + b_preference_json nvarchar(max) not null, + b_updated_at datetime2 null, + primary key (b_user_id, b_module_id, b_schema_type) +); +``` + +模板表沿用相同原则:`s_template` 保存模板身份,`s_template_version` 保存版本、默认字段能力和视图 JSON;模板应用只是生成模块草稿,不直接成为运行时配置。 + +```sql +create table dbo.s_template ( + b_id varchar(80) not null primary key, + b_name nvarchar(200) not null, + b_template_type varchar(30) not null, -- master_data / document / detail / readonly + b_canuse tinyint not null default 1 +); + +create table dbo.s_template_version ( + b_id varchar(100) not null primary key, + b_template_id varchar(80) not null, + b_version_no int not null, + b_default_fields_json nvarchar(max) not null, + b_views_json nvarchar(max) not null, + b_extensions_json nvarchar(max) null, + b_status varchar(20) not null, -- draft / published / retired + b_created_at datetime2 not null default sysdatetime(), + unique (b_template_id, b_version_no) +); +``` + +推荐补充以下约束和发布检查: + +- `s_field.b_module_id`、`s_module_schema.b_module_id`、`s_module_version.b_module_id` 必须引用存在的模块;模块树字段由业务层校验不能成环。 +- `s_module` 的数据源字段由模块类型和通用接口校验组合有效性;查询 SQL 只允许用于受控的只读查询,排序字段必须来自字段定义。 +- `s_module_schema.b_schema_json`、`s_module.b_config_json` 和模板 JSON 使用 `ISJSON` 做基础检查,并由后端 JSON Schema 检查 `schemaVersion`、字段引用和关系引用。 +- `s_module_snapshot`、已发布的 `s_module_version` 和已发布模板版本不可原地修改;回滚通过切换当前有效版本完成。 +- 动态业务表不因为元数据引用而强制建立数据库外键;元数据内部引用使用外键或等效约束,跨 JSON 引用由发布校验保证。 + +### Demo 数据示例 + +以下示例定义一个客户资料模块,业务事实表为 `b_othercompany`,只读查询视图为 `v_b_othercompany`: + +```text +s_module + base.customer → 基础资料 / 客户资料 / data / v_b_othercompany / b_othercompany / b_id + +s_field + base.customer.b_ywid → physical / b_ywid / 客户编码 / code / read+edit+filter+sort + base.customer.b_name → physical / b_name / 客户名称 / text / read+edit+filter+sort + base.customer.b_owner_id → physical / b_owner_id / 业务员 / user_reference / read+edit+filter + +s_module_schema + base.customer / list → schema JSON:列、宽度、排序 + base.customer / form → schema JSON:分组、控件、必填、公式 + base.customer / query → schema JSON:条件字段、组件、操作符 +``` + +列表 JSON 示例: + +```json +{ + "schemaVersion": 1, + "columns": [ + { "field": "b_ywid", "visible": true, "width": 140, "sortable": true }, + { "field": "b_name", "visible": true, "width": 220, "sortable": true }, + { "field": "b_owner_id", "visible": true, "width": 120 } + ], + "defaultSort": [{ "field": "b_name", "direction": "asc" }] +} +``` + +### 现有表到目标表的改造映射 + +- `s_module.b_view_table`、`b_query_sql`、`b_save_table`、`b_key_field`、`b_scope_field`、`b_order_sql` 继续由 `s_module` 保存;删除或收紧无约束的 `b_config_json` 内容。 +- `s_field` 保留模块字段身份,并补充来源映射、语义类型和能力标记;列表、表单、查询的顺序和显示属性保留在对应 `s_module_schema.b_schema_json` 中。 +- `s_module_schema.view/edit/query` 保留为视图配置的主要存储格式,增加 `b_schema_version` 和后端 JSON Schema 校验。 +- `s_relation` 保留已有源/目标字段和删除策略,增加稳定关系编码、用途、加载规则等字段;历史关系先一次性导入,再由发布校验补足引用检查。 +- 现有模块配置可通过一次性导入工具转换;不在新旧表之间长期双写,也不让旧表继续作为第二事实来源。 + +### 不需要整体重做的表 + +- 真实业务表和业务视图继续由 SQL migration 管理,不因为元数据重构而改成 EAV 或通用 JSON。 +- `wf_definition`、`wf_version`、`wf_binding` 及工作流运行表可继续使用;`wf_binding` 需要改为引用模块的已发布版本或在发布时校验模块版本。 +- 现有权限主体和授权表可以保留;权限运行时通过通用接口读取已发布字段和模块定义。 +- 公式不要求新增业务数据表;简单公式随模块快照发布,复杂函数由通用公式接口调用后端函数库。 ### 对现有核心表的评估 **结论:需要重构元数据配置表,但不需要仅因配置器难用而整体重做所有业务数据表。** 当前表结构能表达基本模块、字段和关系;主要问题是职责混放、字段能力分散到视图 JSON,以及缺少可发布的版本边界。 -- **`s_module`**:当前模块身份 / 树结构与查询表、保存表、主键、数据范围字段、默认排序 SQL、查询 SQL 和扩展 JSON 放在同一表。建议将模块身份与数据源 / 查询定义分开建模;移除缺少强约束的通用 `b_config_json`,把常用配置变为有类型的字段或关联实体。模块树优先只保留权威父级和同级顺序;`depth/path` 仅在查询性能有证据需要时保留为派生索引。模块草稿 / 发布时间应由版本模型管理,不靠一个模块级更新时间代表所有子配置状态。 +- **`s_module`**:继续保存模块身份、树结构和默认数据源,因为后端统一通过模块接口读取这些定义;`b_config_json` 只保留少量经过 schema 校验的低频配置,模块草稿 / 发布时间由版本模型管理。 - **`s_field`**:当前主要保存名称、业务类型、启用和顺序,尚未明确区分物理列、查询别名、计算 / 虚拟字段。建议增加明确的字段来源种类与来源映射、语义类型和模块级能力;数据库物理类型由 SQL Server 结构检查获得。表单 / 列表 / 查询中的显示顺序和控件差异放到对应视图字段配置,不塞回字段主定义。 -- **`s_module_schema`**:当前每模块按 `view/edit/query` 保存整份 `nvarchar(max)` JSON,便于早期迭代,但字段参与、字段语义和复杂布局容易混在一起。既然不采用拖拽画布,建议将它替换为视图定义 + 结构化的视图字段 / 分组配置;常用属性使用有类型的列,只有少量确实灵活的扩展留在受校验 JSON。表单不保存行列像素坐标,列表不把所有列属性藏在任意 JSON 节点中。 +- **`s_module_schema`**:继续按 `view/edit/query` 保存视图 JSON,增加结构版本和发布校验;不新增 `s_module_view`、`s_module_view_group` 或 `s_module_view_field`。 - **`s_relation`**:保留关系作为独立元数据,但建议引入稳定关系编码 / 标识,显式描述关系用途、基数、展示 / 加载规则,并对模块和字段引用建立数据库约束。现有删除策略等已表达的业务含义应迁移保留,不因改表而丢失。 - **`s_user_module_pref`**:如保留个性化配置,只允许保存列表列、查询布局等用户偏好覆盖,不得改变模块字段语义、数据源、权限或保存行为。若 Demo 暂无真实个性化需求,可先不纳入新核心模型。 -- **新增发布版本实体**:保存草稿与已发布版本的边界、版本号、校验摘要和不可变运行快照;发布失败不替换当前有效版本。 +- **新增发布版本实体**:使用 `s_module_version` 保存草稿与已发布版本的边界,使用 `s_module_snapshot` 保存不可变运行快照;发布失败不替换当前有效版本。 +- **代码扩展**:不新增模块扩展绑定表;后端统一接口通过 SQL 目录、公式函数库和通用业务处理器协议调用代码实现,模块只使用通用参数和元数据配置。 元数据表之间应使用主键、唯一约束和可表达的外键保障内部引用完整性,并用发布校验补足跨 JSON / 动态数据源的检查。不要为了引用元数据而给所有动态业务表强行建立数据库外键。 ### 规范化与 JSON 边界 - 模块、字段、关系、版本等有稳定身份、查询和引用的对象采用关系表。 -- 表单布局、列表列布局、查询 UI 等灵活结构可存 JSON,但其中只能引用已定义字段和动作。 +- 表单布局、列表列布局、查询 UI、公式和少量扩展属性继续存 JSON,但其中只能引用已定义字段和关系;JSON 必须带 `schemaVersion` 并通过结构校验。 - 发布快照可采用 JSON 作为编译产物,不作为编辑态唯一事实来源。 - 对模块编码、模块内字段编码、模块 / 视图类型、版本号等建立唯一约束;可表达的元数据引用建立外键或等效约束,并由发布校验再次检查。 - 明确删除、改名和关系变更策略,不依赖前端操作顺序保证一致性。 @@ -150,7 +366,7 @@ SQL Server 中的真实业务表和视图继续作为业务数据事实来源。 - **列表**:采用列配置表 / 字段矩阵,集中设置是否显示、顺序、宽度、格式、对齐、排序和汇总等属性;支持批量添加、移除和套用模板。 - **预览**:配置面板旁提供实时预览,但预览仅用于检查结果,不作为拖放编辑画布。 - **元数据**:保存语义结构和字段引用,不保存像素坐标、拖拽路径或依赖编辑器内部实现的状态。 -- **高级场景**:通过分组、布局模板、字段显示规则及明确的扩展配置解决;不以重新引入自由拖拽作为兜底。 +- **高级场景**:通过分组、布局模板、字段显示规则及通用接口配置解决;不以重新引入自由拖拽作为兜底。 默认规则: @@ -175,13 +391,13 @@ SQL Server 中的真实业务表和视图继续作为业务数据事实来源。 不做新旧格式长期并行或渐进兼容;按依赖顺序拆分实施任务,便于验证: -1. **确定元数据契约**:模块类型、字段语义、关系、视图、默认覆盖、发布和动作扩展模型。 -2. **重建元数据数据库**:新表、约束、种子 / migration;决定 Demo 配置重建还是一次性导入。 -3. **实现后端目录与编译器**:读取、校验、默认合成、发布、版本切换、缓存失效和回滚。 +1. **确定元数据契约**:模块类型、字段语义、关系、视图、默认覆盖、发布和通用接口契约。 +2. **重建元数据数据库**:调整模块、字段、视图 JSON 和关系表,新增版本与快照表,补充主键 / 唯一约束 / 引用约束;决定 Demo 配置重建还是一次性导入。 +3. **实现后端目录与编译器**:读取物理结构、校验字段和视图 JSON、默认合成、生成快照、发布、版本切换、缓存失效和回滚。 4. **替换配置工作台**:数据源检查、字段导入、模板初始化、能力矩阵、预览、发布。 5. **替换运行时消费链路**:列表、查询、表单、保存统一读取发布定义。 -6. **接入扩展能力**:权限、关系、公式、工作流、导出和业务动作。 -7. **删除旧路径**:完成导入或确认弃用后,删除旧 Schema 适配和重复逻辑。 +6. **接入通用能力**:在统一模块接口中接入列表、查询、保存、权限、关系、工作流和公式的标准实现,并接入统一 SQL、函数库和业务处理器协议。 +7. **删除旧路径**:完成一次性导入或确认弃用后,删除旧 `s_module_schema` JSON 结构适配和重复逻辑;保留新格式下的同一张表作为唯一视图配置来源。 虽然不需要渐进兼容,仍建议用一个基础资料、一个主从单据、一个只读查询模块验证新契约,再批量建立 Demo 配置。这是新架构验收,不是要求新旧架构并行运行。 @@ -194,22 +410,25 @@ SQL Server 中的真实业务表和视图继续作为业务数据事实来源。 - 默认配置可重复生成,且不覆盖显式配置。 - 无效字段引用、数据源列缺失、关系目标错误、非法查询规则在发布时给出明确错误。 - 运行时只消费已发布版本;发布失败不影响当前可用版本。 +- 编辑态使用模块、字段、视图 JSON 和关系表;运行态使用对应的不可变发布快照。 +- 列表、查询、保存、权限、关系、工作流和公式均可使用元数据默认实现,并可通过统一 SQL、函数库和业务处理器协议补充复杂逻辑。 +- 统一接口的参数、字段引用和依赖关系在发布时校验;接口执行失败有明确错误和审计信息。 - 简单 CRUD 使用通用运行时;复杂业务动作由明确的后端处理器完成。 - 真实 SQL Server 业务表仍是业务数据事实来源,不引入 EAV 业务数据主存储。 - 旧元数据路径已删除或仅作为一次性导入工具,不存在双重事实来源。 ## 10. 需要同步修订的文档 -目前架构文档存在方向冲突:部分草案提出以业务域代码包取代元数据运行时,开发规范与现有系统仍要求模块 / 字段 / 界面元数据驱动通用模块。本方案评审通过后应统一修订: +目前架构文档存在方向冲突:部分草案提出以业务域代码包取代元数据运行时,而本方案采用元数据主导、通用接口承载复杂逻辑的模式。本方案评审通过后应统一修订: -- `FMS新系统核心架构设计.md`:明确元数据驱动是通用模块核心,SQL / 后端动作是扩展机制。 -- `FMS新系统核心架构实施计划.md`:改为元数据数据库、编译器、工作台和运行时重构任务。 +- `FMS新系统核心架构设计.md`:明确元数据运行时是通用能力核心,业务域代码通过统一模块接口、SQL 和业务处理器协议接入复杂逻辑。 +- `FMS新系统核心架构实施计划.md`:增加元数据数据库、编译器、工作台、运行时和通用接口实现任务。 - `FMS新系统核心表结构设计.md`:重新定义元数据表,不再将现有 `s_module`、`s_field`、`s_module_schema` 作为不可变前提。 -- `开发规范.md`:明确元数据契约、发布定义、查询 / 保存边界和动作扩展约定。 +- `开发规范.md`:明确元数据契约、发布定义、查询 / 保存边界和通用接口约定。 ## 11. 假设与待定项 - 暂保留 SQL Server 作为数据库目标;允许重做元数据表,不主动改变租户数据库路由模式。 - 元数据和 Demo 配置可重建;是否保留样例数据,实施前决定重建或一次性导入。 - 本文确定架构方向,不冻结最终 DDL、API、类名或 Vue 目录;这些在元数据契约评审后设计。 -- 权限、工作流、公式、多语言是否随模块发布版本整体冻结,需结合实际使用方式决定,避免第一版过度设计。 +- 需要确定权限、工作流、公式、多语言及通用接口实现是否随模块发布版本整体冻结,并定义接口版本兼容、事务边界、错误模型和规则优先级。 diff --git a/code/g3soft-erp/web/.bc-bg.png b/code/g3soft-erp/web/.bc-bg.png new file mode 100644 index 00000000..30329dd2 Binary files /dev/null and b/code/g3soft-erp/web/.bc-bg.png differ diff --git a/code/g3soft-erp/web/.bc-normal.png b/code/g3soft-erp/web/.bc-normal.png new file mode 100644 index 00000000..4109fc46 Binary files /dev/null and b/code/g3soft-erp/web/.bc-normal.png differ diff --git a/code/g3soft-erp/web/.bc.png b/code/g3soft-erp/web/.bc.png new file mode 100644 index 00000000..e5d8a681 Binary files /dev/null and b/code/g3soft-erp/web/.bc.png differ diff --git a/code/g3soft-erp/web/.bc2.png b/code/g3soft-erp/web/.bc2.png new file mode 100644 index 00000000..c462225d Binary files /dev/null and b/code/g3soft-erp/web/.bc2.png differ diff --git a/code/g3soft-erp/web/.h-bg.png b/code/g3soft-erp/web/.h-bg.png new file mode 100644 index 00000000..e7e3d234 Binary files /dev/null and b/code/g3soft-erp/web/.h-bg.png differ diff --git a/code/g3soft-erp/web/.h-normal.png b/code/g3soft-erp/web/.h-normal.png new file mode 100644 index 00000000..38aeec93 Binary files /dev/null and b/code/g3soft-erp/web/.h-normal.png differ diff --git a/code/g3soft-erp/web/index.html b/code/g3soft-erp/web/index.html index 2d944557..9aeeb0bd 100644 --- a/code/g3soft-erp/web/index.html +++ b/code/g3soft-erp/web/index.html @@ -4,9 +4,321 @@ G3Soft ERP + + + -
+
+ +
+ +
+
+ + + +
+
+ diff --git a/code/g3soft-erp/web/src/App.vue b/code/g3soft-erp/web/src/App.vue index a78fb6fd..07cbbb5a 100644 --- a/code/g3soft-erp/web/src/App.vue +++ b/code/g3soft-erp/web/src/App.vue @@ -1,7 +1,48 @@ + + diff --git a/code/g3soft-erp/web/src/composables/useOnline.ts b/code/g3soft-erp/web/src/composables/useOnline.ts new file mode 100644 index 00000000..5fa60ff3 --- /dev/null +++ b/code/g3soft-erp/web/src/composables/useOnline.ts @@ -0,0 +1,63 @@ +import { computed, onBeforeUnmount, readonly, ref, type ComputedRef, type Ref } from 'vue' + +/** + * 网络在线状态。 + * + * 对照 fms-vue 的 App.vue —— 它在应用根监听 window 的 online/offline, + * 断网时盖一层离线缺省页(FallbackPage status="offline"),恢复后自动移除。 + * + * 为什么用 navigator.onLine + 事件监听,而不是轮询: + * 轮询会持续发请求,白耗流量与电量;这两个 API 是浏览器原生事件驱动, + * 零轮询、零定时器,断网/恢复的响应是即时的。性能开销可忽略。 + * + * ⚠️ 已知局限(fms-vue 也记了同一条):navigator.onLine 只反映【本机是否连上网】, + * 连在局域网但服务端不可达时它仍报在线。那种场景由接口层兜底 —— + * request.ts 对「无 status(服务端不可达)」统一弹「网络异常」提示。 + * 所以本 composable 不需要、也不应该去做健康检查。 + */ + +/** 模块级单例:多处读取共用一份状态与一套监听 */ +const online = ref(true) +let listenerCount = 0 + +function sync(): void { + online.value = navigator.onLine +} + +/** 首个使用者挂监听,最后一个使用者解绑 —— 避免重复注册 */ +function bind(): void { + if (listenerCount === 0) { + sync() + window.addEventListener('online', sync) + window.addEventListener('offline', sync) + } + listenerCount += 1 +} + +function unbind(): void { + listenerCount -= 1 + if (listenerCount > 0) return + + window.removeEventListener('online', sync) + window.removeEventListener('offline', sync) +} + +/** 只读的在线态(对外只读,写入只能由浏览器的 online/offline 事件驱动) */ +const isOnline = readonly(online) + +/** 离线态:取反,供模板直接 v-if */ +const isOffline: ComputedRef = computed(() => !online.value) + +export interface UseOnlineReturn { + /** 当前是否在线 */ + isOnline: Readonly> + /** 当前是否离线 */ + isOffline: ComputedRef +} + +export function useOnline(): UseOnlineReturn { + bind() + onBeforeUnmount(unbind) + + return { isOnline, isOffline } +} diff --git a/code/g3soft-erp/web/src/layouts/LayoutBreadcrumb.vue b/code/g3soft-erp/web/src/layouts/LayoutBreadcrumb.vue index d29259b6..5ee981f2 100644 --- a/code/g3soft-erp/web/src/layouts/LayoutBreadcrumb.vue +++ b/code/g3soft-erp/web/src/layouts/LayoutBreadcrumb.vue @@ -3,31 +3,59 @@ import { computed } from 'vue' import { useRoute } from 'vue-router' import { House } from '@lucide/vue' import { usePreferencesStore } from '@/preferences' -import { findMenuItemByPath, findParentKeyByPath } from '@/router' -import { menuItems } from '@/constants/menu' +import { findMenuItemByKey, findMenuItemByPath, findParentKeysByPath } from '@/router' + +/** 面包屑单项:有 path 的是可点的父级,没有的是当前页。 */ +interface Crumb { + title: string + path?: string +} /** * 面包屑。 - * 数据来自菜单常量:父级分组 + 当前页,与侧栏同源,不额外维护。 + * 数据来自菜单常量:祖先分组链 + 当前页,与侧栏同源,不额外维护。 */ const route = useRoute() const prefStore = usePreferencesStore() const pref = computed(() => prefStore.preferences) -/** 当前路径对应的祖先链:[分组, 当前页] */ -const crumbs = computed(() => { - const list: Array<{ title: string; path?: string }> = [] +/** + * 当前路径对应的祖先链:[...分组, 当前页]。 + * + * 父级用 findParentKeysByPath(返回【完整】祖先 key 链)而不是 + * findParentKeyByPath(只取 [0] = 顶级)—— 后者在多级菜单下会漏掉中间层: + * 「海运 → 订舱业务 → 订舱单」只会显示成「海运 / 订舱单」。 + */ +const crumbs = computed(() => { + const list: Crumb[] = [] if (pref.value.breadcrumb.showHome) { list.push({ title: '首页', path: '/' }) } - const current = findMenuItemByPath(route.path) - const parentKey = findParentKeyByPath(route.path) - const parent = parentKey ? menuItems.find((item) => item.key === parentKey) : undefined + // 1) 菜单里的完整祖先链(顶级 → 最近一级分组) + for (const key of findParentKeysByPath(route.path)) { + const node = findMenuItemByKey(key) + if (node) list.push({ title: node.title, path: node.path }) + } - if (parent) list.push({ title: parent.title, path: parent.path }) - if (current && current.key !== parent?.key) list.push({ title: current.title }) + const current = findMenuItemByPath(route.path) + + // 2) 菜单里查不到祖先(页面不在菜单树 / 菜单与路由不同步)时, + // 用路由层级兜底:route.matched 上带 title 的祖先记录即父级。 + // 当前页不在这里补(由下面统一追加),避免自链接。 + if (!current && list.length === 0) { + for (const record of route.matched) { + if (record.path === route.path) break + const title = record.meta.title as string | undefined + if (title) list.push({ title, path: record.path }) + } + } + + // 3) 当前页:菜单里查得到就用菜单名(业务页); + // 查不到(403/404/500 等缺省页、详情页)回退到 route.meta.title。 + const currentTitle = current?.title ?? (route.meta.title as string | undefined) + if (currentTitle) list.push({ title: currentTitle }) return list }) @@ -50,11 +78,16 @@ const visible = computed(() => { / - - {{ crumb.title }} + + + + {{ crumb.title }} + - {{ crumb.title }} + + {{ crumb.title }} + @@ -65,13 +98,8 @@ const visible = computed(() => { align-items: center; gap: 6px; min-width: 0; - font-size: 13px; - - &.is-background { - padding: 4px 10px; - background: var(--g3-color-fill-hover, #f2f3f5); - border-radius: 6px; - } + /* 字号:15px —— 比顶栏 18px 的图标按钮小一档,层次清楚又不至于太弱 */ + font-size: 15px; &__sep { color: var(--g3-color-text-tertiary, #86909c); @@ -80,7 +108,6 @@ const visible = computed(() => { &__item { display: inline-flex; align-items: center; - gap: 4px; color: var(--g3-color-text-secondary, #4e5969); text-decoration: none; @@ -92,5 +119,113 @@ const visible = computed(() => { color: var(--g3-color-text, #1f2329); } } + + &__content { + display: inline-flex; + align-items: center; + gap: 4px; + } + + /* ---- background 风格 ---- + * 照搬 vben 的 breadcrumb-background.vue:整条不是「灰底圆角块」,而是 + * 一段段 accent 底色的「>」形(面包屑步骤条)。段与段之间不再用 / 分隔符, + * 靠两块 14px 边框三角拼接: + * ::before 放在段左侧 28px 处,左三角透明 → 形成向左的凹口; + * ::after 放在段右侧,只留左三角着色 → 形成指向右的尖角。 + * 14px 边框 × 上下两半 = 28px,正好等于段高 h-7(28px)。 */ + &.is-background { + gap: 0; + font-size: 13px; + + /* 有箭头就没有分隔符了 */ + .layout-breadcrumb__sep { + display: none; + } + + .layout-breadcrumb__item { + position: relative; + display: flex; + align-items: center; + height: 28px; + /* mr-9(36px):给右箭头占的 28px 留位,另留 8px 视觉间隙 */ + margin-right: 36px; + padding: 0 8px 0 5px; + color: var(--g3-color-text-secondary, #4e5969); + background: var(--g3-color-accent, #f2f3f5); + transition: background-color 0.2s ease; + + &::before, + &::after { + content: ''; + position: absolute; + top: 0; + width: 0; + height: 0; + border: 14px solid var(--g3-color-accent, #f2f3f5); + } + + &::before { + left: -28px; + z-index: 10; + border-left-color: transparent; + } + + &::after { + left: 100%; + border-color: transparent; + border-left-color: var(--g3-color-accent, #f2f3f5); + } + + /* 首段:左端不开口,左侧留白加大并做左圆角 */ + &:first-child { + padding-left: 15px; + border-radius: 2px 0 0 2px; + + &::before { + border: none; + } + } + + /* 末段(当前页):不做右箭头,右侧留白加大并做右圆角 */ + &:last-child { + padding-right: 15px; + border-radius: 0 2px 2px 0; + + &::after { + border: none; + } + } + + &.is-current { + color: var(--g3-color-foreground, #373c43); + } + + /* 悬停只换底色,不改文字色(对齐 vben:当前页不可点,不参与 hover) */ + &:not(:last-child):hover { + color: var(--g3-color-text-secondary, #4e5969); + background: var(--g3-color-accent-hover, #e3e5e8); + + &::before { + border-color: var(--g3-color-accent-hover, #e3e5e8); + border-left-color: transparent; + } + + &::after { + border-left-color: var(--g3-color-accent-hover, #e3e5e8); + } + } + } + + /* 内容整体左移,视觉上落进「>」的口子里(照 vben 的 -ml-3 / 首段 -ml-1) */ + .layout-breadcrumb__content { + position: relative; + z-index: 10; + margin-left: -12px; + } + + .layout-breadcrumb__item:first-child .layout-breadcrumb__content { + margin-left: -4px; + } + } } diff --git a/code/g3soft-erp/web/src/layouts/LayoutHeader.vue b/code/g3soft-erp/web/src/layouts/LayoutHeader.vue index 935ade23..b9e1c5a1 100644 --- a/code/g3soft-erp/web/src/layouts/LayoutHeader.vue +++ b/code/g3soft-erp/web/src/layouts/LayoutHeader.vue @@ -1,22 +1,27 @@