From 8e75c7989af230614ce12682c813ca40e4c9f89a Mon Sep 17 00:00:00 2001 From: oneao Date: Wed, 19 Aug 2026 21:39:33 +0800 Subject: [PATCH] 20260819213933 --- code/fms/FMS新系统核心表结构设计.md | 957 ++++++++---------- code/fms/fms-vue/pnpm-lock.yaml | 9 +- code/fms/fms-vue/pnpm-workspace.yaml | 10 + code/test-demo/AI_CONTEXT.md | 92 +- code/test-demo/index.html | 2 +- code/test-demo/src/App.tsx | 376 ++++--- code/test-demo/src/assets/hero.png | Bin 13057 -> 0 bytes code/test-demo/src/assets/react.svg | 1 - code/test-demo/src/assets/vite.svg | 1 - .../src/components/AutoCodePanel.tsx | 267 +++++ .../src/components/DataSourceDrawer.tsx | 122 --- code/test-demo/src/components/EntityPanel.tsx | 489 +++++++++ .../src/components/FieldConfigList.tsx | 488 --------- code/test-demo/src/components/FormPreview.tsx | 175 ---- code/test-demo/src/components/MenuPanel.tsx | 263 +++++ code/test-demo/src/components/MetaTree.tsx | 310 ++++++ code/test-demo/src/components/ModulePanel.tsx | 279 +++++ .../src/components/ModuleSettings.tsx | 86 -- code/test-demo/src/components/NodeEditor.tsx | 546 ++++++++++ .../src/components/PermissionPanel.tsx | 386 +++++++ .../test-demo/src/components/TablePreview.tsx | 173 ---- code/test-demo/src/components/ui.tsx | 117 +++ code/test-demo/src/data/mock.ts | 345 +++++++ code/test-demo/src/data/moduleData.ts | 49 - code/test-demo/src/index.css | 521 ---------- code/test-demo/src/main.tsx | 4 +- code/test-demo/src/types/index.ts | 218 +++- 27 files changed, 3852 insertions(+), 2434 deletions(-) delete mode 100644 code/test-demo/src/assets/hero.png delete mode 100644 code/test-demo/src/assets/react.svg delete mode 100644 code/test-demo/src/assets/vite.svg create mode 100644 code/test-demo/src/components/AutoCodePanel.tsx delete mode 100644 code/test-demo/src/components/DataSourceDrawer.tsx create mode 100644 code/test-demo/src/components/EntityPanel.tsx delete mode 100644 code/test-demo/src/components/FieldConfigList.tsx delete mode 100644 code/test-demo/src/components/FormPreview.tsx create mode 100644 code/test-demo/src/components/MenuPanel.tsx create mode 100644 code/test-demo/src/components/MetaTree.tsx create mode 100644 code/test-demo/src/components/ModulePanel.tsx delete mode 100644 code/test-demo/src/components/ModuleSettings.tsx create mode 100644 code/test-demo/src/components/NodeEditor.tsx create mode 100644 code/test-demo/src/components/PermissionPanel.tsx delete mode 100644 code/test-demo/src/components/TablePreview.tsx create mode 100644 code/test-demo/src/components/ui.tsx create mode 100644 code/test-demo/src/data/mock.ts delete mode 100644 code/test-demo/src/data/moduleData.ts diff --git a/code/fms/FMS新系统核心表结构设计.md b/code/fms/FMS新系统核心表结构设计.md index b3c6df8c..e327c9ff 100644 --- a/code/fms/FMS新系统核心表结构设计.md +++ b/code/fms/FMS新系统核心表结构设计.md @@ -1,645 +1,566 @@ -# FMS 新系统核心表结构设计 +# FMS 新系统模块区核心表结构设计 -## 1. 说明 +> 本方案为模块元数据中心的重新设计,表名与字段名命名风格参考旧系统: +> 平台表以 `s_` 开头,字段以 `b_` 开头,复合词采用驼峰拼接(如 `b_viewtable`、`b_inputdatetime`),逻辑关联字段使用 `b_xxx_id`。 -本文档给出 FMS 新系统核心平台表的 SQL Server 建表语句,范围包括元数据中心(模块、实体、字段、字段分组、关系、页面菜单、多语言)、Schema Compiler、权限中心、技术日志、登录日志与业务审计。 +## 1. 设计原则 -设计原则: +针对旧系统 `s_module` 职责混杂的问题,本方案坚持四个分离: -- 业务实体(订舱、集装箱、发票等)由 Schema Compiler 根据 `s_module_entity` + `s_module_field` 生成真实物理表,本文档只定义平台核心表。 -- 实体是数据和字段的所有者,页面只是实体的一种展示方式;字段挂实体(`s_module_entity`),不挂菜单。 -- 页面和菜单合并为一条 `s_module_menu` 记录(导航节点 = 页面入口),用 `b_menu_type` 区分节点性质。 -- 元数据变更通过服务层校验后立即生效,DDL 同步执行、失败回滚,不做异步迁移任务表,也不建立配置变更集、版本快照或运行时状态表。 +1. **模块与实体分离**:`s_module` 只表达业务能力边界与依赖关系,不承载表、路由、菜单;数据归属由 `s_entity` 承担。 +2. **实体与字段分离**:实体是数据所有者,声明查询源/保存源/主键字段;字段挂实体,同时携带数据库列定义与 UI 行为。 +3. **菜单与模块分离**:`s_menu` 是纯导航树,只引用实体或固定页面,不掺入数据源配置。 +4. **字段定义与视图配置合并**:旧系统把字段拆成 `s_module_field` + `_edit` + `_list` + `_query` 四张表,维护极易失配;本方案合并为单表 `s_field`,用标志列表达列表/表单/查询行为。 -约定如下: +其余约定: -- 所有表名以 `s_` 开头,字段名以 `b_` 开头。 -- 表名、字段名、索引名和约束名统一使用 `snake_case`。 -- 不创建物理外键,关联字段只作为逻辑外键使用。 -- 逻辑外键必须创建必要的普通索引或唯一索引。 -- 一个租户使用一个独立业务数据库,普通业务表不重复保存 `b_tenant_id` 或 `b_org_id`;集中技术日志和登录日志允许使用 `b_org_id` 标识日志来源。 -- 内部主键统一使用 `bigint identity(1, 1)`,跨环境标识使用稳定的 `b_*_key`。 -- 同库内的逻辑关联 ID(例如 `b_form_id`)统一使用 `bigint`,不使用 `uniqueidentifier`。 -- 时间点统一使用 `datetime2(3)`,应用层按 UTC 写入。 -- 稳定业务标识统一使用 `b_*_key`,跨环境导入导出不得依赖内部 `b_id`。 -- 扩展内容统一使用 `nvarchar(max)` 普通文本保存,由应用层按业务约定解析和校验,不使用 JSON 字段类型。 -- 原布尔语义字段统一使用 `tinyint`,取值约定为 `0`(否)或 `1`(是)。 -- 核心业务字段使用真实数据库列,不采用全量 EAV;不设置无业务含义的通用扩展列(如 `b_bz1~n`、`mx_varchar_n`),字段必须有明确的 `b_field_key` 与语义。 -- 下面的 `b_*_id`、`b_*_key` 关联均为逻辑外键,由应用服务负责校验一致性。 +- 内部主键 `b_id bigint identity(1, 1)`,跨环境稳定标识使用 `b_code`(参考旧系统)。 +- 元数据表保留物理外键(低频、高一致性,防脏数据);若担心部署顺序,可退化为逻辑外键并保留索引。 +- 布尔语义使用 `int`(0/1),与旧系统 `s_module` 风格一致。 +- 时间使用 `datetime2`,默认 `sysdatetime()`。 +- 审计字段:定义类表保留 `b_inputuser_id / b_inputdatetime / b_updateuser_id / b_updatedatetime`;关联类表只保留 `b_inputdatetime`。 +- 模块归属保留树形(`b_parent_id`,仅用于业务分组),模块间依赖使用独立的 DAG 表,两者语义互不混淆。 -## 2. 元数据中心 +## 2. 表清单 -### 2.1 模块 +| # | 表名 | 说明 | +| --- | --- | --- | +| 1 | `s_module` | 模块(业务能力边界 + 业务分组树) | +| 2 | `s_module_dependency` | 模块依赖(DAG,禁止循环) | +| 3 | `s_entity` | 实体(数据所有者,声明查/存数据源) | +| 4 | `s_field` | 字段(列定义 + 列表/表单/查询行为合并单表) | +| 5 | `s_field_group` | 字段分组(实体级) | +| 6 | `s_relation` | 实体关系(引用/主子,驱动外键校验与级联) | +| 7 | `s_menu` | 菜单/页面(纯导航,挂模块做组织维度) | +| 8 | `s_autocode` | 自动编码规则 | +| 9 | `s_role` | 角色 | +| 10 | `s_user_role` | 用户-角色 | +| 11 | `s_power` | 权限点(menu/entity/action/field) | +| 12 | `s_role_power` | 角色-权限 | +| 13 | `s_i18n` | 多语言资源 | +| 14 | `s_log_technical` | 技术日志(请求链路/SQL/异常/性能) | +| 15 | `s_log_login` | 登录日志(登录/退出/失败/锁定) | +| 16 | `s_log_audit` | 业务审计事件 | +| 17 | `s_log_audit_field` | 审计字段变更明细 | +## 3. 各表结构 -模块是业务能力边界,不是数据库表的别名。模块只保存身份、依赖与启用状态,不承载菜单、路由或数据源职责。 +### 3.1 s_module 模块 + +只保存模块身份、业务分组与启用状态,不承载菜单、路由或数据源。 ```sql -create table dbo.s_module ( - b_id bigint identity(1, 1) not null, -- 内部自增主键 - b_module_key nvarchar(128) not null, -- 模块稳定业务标识(如 ocean、finance、master-data) - b_name_key nvarchar(256) not null, -- 模块显示名多语言 key - b_description nvarchar(1000) null, -- 模块描述 - b_origin nvarchar(32) not null -- 来源:platform 平台内置 / tenant 租户定制 - constraint df_s_module_origin default N'platform', - b_enabled tinyint not null -- 是否启用(1启用/0停用) - constraint df_s_module_enabled default 1, - b_sort_no int not null -- 模块排序号 - constraint df_s_module_sort_no default 0, - b_icon_key nvarchar(128) null, -- 模块图标 key - b_config_text nvarchar(max) null, -- 模块扩展配置(应用自定义解析) - b_created_at datetime2(3) not null -- 创建时间(UTC) - constraint df_s_module_created_at default sysutcdatetime(), - b_created_by nvarchar(128) not null, -- 创建人 +create table s_module ( + b_id bigint identity(1, 1) not null, -- 内部主键 + b_code varchar(50) not null, -- 模块编码(如 ocean/finance/master-data) + b_name nvarchar(100) not null, -- 模块名称 + b_i18n varchar(150) null, -- 显示名多语言 key + b_parent_id bigint null, -- 父模块(仅业务分组,不承担导航) + b_module_type varchar(20) not null default 'business', -- 类型:platform 平台/tenant 租户/business 业务 + b_canuse int not null default 1, -- 是否启用 + b_xh int not null default 0, -- 排序号 + b_icon varchar(50) null, -- 图标 + b_bz nvarchar(1000) null, -- 描述 + b_inputuser_id bigint null, + b_inputdatetime datetime2 null default sysdatetime(), + b_updateuser_id bigint null, + b_updatedatetime datetime2 null, constraint pk_s_module primary key (b_id), - constraint ux_s_module_key unique (b_module_key) + constraint ux_s_module_code unique (b_code), + constraint fk_s_module_parent foreign key (b_parent_id) references s_module (b_id) ); -create index ix_s_module_key - on dbo.s_module (b_module_key, b_enabled); +create index ix_s_module_parent on s_module (b_parent_id, b_xh, b_canuse); ``` -### 2.2 模块依赖 +### 3.2 s_module_dependency 模块依赖 -模块依赖必须形成有向无环图,不允许循环依赖。 +模块依赖必须形成有向无环图,由服务层校验禁止循环。 ```sql -create table dbo.s_module_dependency ( - b_id bigint identity(1, 1) not null, -- 内部自增主键 - b_module_key nvarchar(128) not null, -- 依赖方模块 key(如 ocean) - b_depends_on_module_key nvarchar(128) not null, -- 被依赖模块 key(如 master-data) - b_created_at datetime2(3) not null -- 创建时间(UTC) - constraint df_s_module_dependency_created_at default sysutcdatetime(), - b_created_by nvarchar(128) not null, -- 创建人 +create table s_module_dependency ( + b_id bigint identity(1, 1) not null, -- 内部主键 + b_module_id bigint not null, -- 依赖方模块 + b_depend_module_id bigint not null, -- 被依赖模块 + b_inputdatetime datetime2 null default sysdatetime(), constraint pk_s_module_dependency primary key (b_id), - constraint ux_s_module_dependency_key unique ( - b_module_key, - b_depends_on_module_key - ) + constraint ux_s_module_dependency unique (b_module_id, b_depend_module_id), + constraint fk_s_module_dependency_module foreign key (b_module_id) references s_module (b_id), + constraint fk_s_module_dependency_depend foreign key (b_depend_module_id) references s_module (b_id) ); -create index ix_s_module_dependency_depends_on - on dbo.s_module_dependency (b_depends_on_module_key); +create index ix_s_module_dependency_depend on s_module_dependency (b_depend_module_id); ``` -### 2.3 实体 +### 3.3 s_entity 实体 -实体是数据和字段的所有者,同时声明"查询数据源"和"保存数据源"。查询源可以是真实表,也可以是带关联/聚合的 SQL 视图(视图只读,用于列表展示);保存目标必须是真实表。 +实体是数据所有者,同时声明查询数据源与保存数据源;主子表通过 `b_parent_id` 表达(与菜单树无关)。 ```sql -create table dbo.s_module_entity ( - b_id bigint identity(1, 1) not null, -- 内部自增主键 - b_entity_key nvarchar(192) not null, -- 实体稳定业务标识(如 ocean.booking) - b_module_key nvarchar(128) not null, -- 所属模块 key - b_name_key nvarchar(256) not null, -- 实体显示名多语言 key - b_description nvarchar(1000) null, -- 实体描述 - b_view_table sysname not null, -- 查询数据源:真实表或 SQL 视图(如 ocean_booking / v_booking_with_fee) - b_save_table sysname null, -- 保存目标:真实表(查询源为视图时必填,只读实体可为空) - b_key_field sysname null, -- 保存对象主键字段(如 b_id) - b_delete_policy nvarchar(32) not null -- 删除策略(默认 lifecycle) - constraint df_s_module_entity_delete_policy default N'lifecycle', - b_origin nvarchar(32) not null -- 来源:platform / tenant - constraint df_s_module_entity_origin default N'platform', - b_enabled tinyint not null -- 是否启用 - constraint df_s_module_entity_enabled default 1, - b_config_text nvarchar(max) null, -- 实体扩展配置 - b_created_at datetime2(3) not null -- 创建时间(UTC) - constraint df_s_module_entity_created_at default sysutcdatetime(), - b_created_by nvarchar(128) not null, -- 创建人 - constraint pk_s_module_entity primary key (b_id), - constraint ux_s_module_entity_key unique (b_entity_key) +create table s_entity ( + b_id bigint identity(1, 1) not null, -- 内部主键 + b_module_id bigint not null, -- 所属模块 + b_parent_id bigint null, -- 父实体(主子表关系) + b_code varchar(50) not null, -- 实体编码(如 ocean_booking) + b_name nvarchar(100) not null, -- 实体名称 + b_i18n varchar(150) null, -- 显示名多语言 key + b_viewtable varchar(128) null, -- 查询数据源:真实表或只读视图 + b_savetable varchar(128) null, -- 保存目标:真实表(纯查询实体可空) + b_keyfield varchar(50) null, -- 保存对象主键字段(如 b_id) + b_entity_type varchar(20) not null default 'table', -- table 简单实体/view 复杂列表/query 纯查询 + b_delete_policy varchar(20) not null default 'lifecycle', -- restrict/lifecycle/cascade/hard + b_canuse int not null default 1, -- 是否启用 + b_xh int not null default 0, -- 排序号 + b_bz nvarchar(1000) null, -- 描述 + b_inputuser_id bigint null, + b_inputdatetime datetime2 null default sysdatetime(), + b_updateuser_id bigint null, + b_updatedatetime datetime2 null, + constraint pk_s_entity primary key (b_id), + constraint ux_s_entity_module_code unique (b_module_id, b_code), + constraint fk_s_entity_module foreign key (b_module_id) references s_module (b_id), + constraint fk_s_entity_parent foreign key (b_parent_id) references s_entity (b_id) ); -create index ix_s_module_entity_module_key - on dbo.s_module_entity (b_module_key, b_enabled); - -create index ix_s_module_entity_save_table - on dbo.s_module_entity (b_save_table); +create index ix_s_entity_parent on s_entity (b_parent_id); +create index ix_s_entity_savetable on s_entity (b_savetable); ``` -实体的"查/存"配置方式: +实体的查/存配置方式: ```text -简单实体: b_view_table = b_save_table = 同一张真实表 -复杂列表: b_view_table = 带关联/聚合的 SQL 视图(只读), b_save_table = 真实表 -纯查询实体: b_view_table = 视图或表, b_save_table = 空(只读,不可保存) +简单实体: b_viewtable = b_savetable = 同一张真实表 +复杂列表: b_viewtable = 带关联/聚合的 SQL 视图(只读), b_savetable = 真实表 +纯查询实体: b_viewtable = 视图或表, b_savetable = 空(只读) ``` 删除策略取值(`b_delete_policy`): ```text -restrictWhenReferenced 主数据:有引用时拒绝删除,只能停用或合并 -lifecycle 业务单据:草稿且无下游时可删,正式后只能取消/作废 -cascadeOwned 主表拥有的从数据:随主表同一事务级联删除 -hardDelete 临时记录:校验权限后直接物理删除 +restrict 主数据:有引用时拒绝删除,只能停用或合并 +lifecycle 业务单据:草稿且无下游时可删,正式后只能取消/作废 +cascade 主表拥有的从数据:随主表同一事务级联删除 +hard 临时记录:校验权限后直接物理删除 ``` -### 2.4 字段 +### 3.4 s_field 字段 -字段属于实体,同时携带数据库列定义、UI 行为与业务语义(供 UI 和 AI 理解)。 +合并旧系统 `s_module_field` + `_edit` + `_list` + `_query` 四张表,字段增删改只需维护一处。 ```sql -create table dbo.s_module_field ( - b_id bigint identity(1, 1) not null, -- 内部自增主键 - b_entity_key nvarchar(192) not null, -- 所属实体 key - b_field_key nvarchar(128) not null, -- 字段稳定业务标识(如 carrier_id) - b_name_key nvarchar(256) not null, -- 字段显示名多语言 key - b_label nvarchar(256) not null, -- 字段标签(可随时改,不改数据库) - b_description nvarchar(1000) null, -- 字段描述 - b_physical_column_name sysname not null, -- 真实数据库列名(通常与 b_field_key 相同) - b_data_type nvarchar(64) not null, -- 存储数据类型(如 varchar/decimal/datetime) - b_semantic_type nvarchar(128) null, -- 语义类型(如 email/money/phone,供 UI/AI 理解) - b_write_mode nvarchar(32) not null -- 后端写入模式(如 direct/compute/readonly) - constraint df_s_module_field_write_mode default N'direct', - b_required tinyint not null -- 是否必填 - constraint df_s_module_field_required default 0, - b_enabled tinyint not null -- 是否启用(停用则隐藏并保留数据) - constraint df_s_module_field_enabled default 1, - b_queryable tinyint not null -- 是否出现在查询条件中 - constraint df_s_module_field_queryable default 0, - b_sortable tinyint not null -- 是否可排序 - constraint df_s_module_field_sortable default 0, - b_list_visible tinyint not null -- 是否显示在固定列表组件 - constraint df_s_module_field_list_visible default 1, - b_edit_visible tinyint not null -- 是否显示在固定编辑组件 - constraint df_s_module_field_edit_visible default 1, - b_readonly tinyint not null -- 编辑组件中是否只读 - constraint df_s_module_field_readonly default 0, - b_sort_no int not null -- 列表/表单默认排序 - constraint df_s_module_field_sort_no default 0, - b_group_key nvarchar(128) null, -- 所属分组 key(逻辑外键,空为未分组) - b_width int null, -- 列表列宽(px) - b_component_type nvarchar(64) null, -- 固定表单组件类型(input/select/date) - b_auditable tinyint not null -- 是否记录字段级审计 - constraint df_s_module_field_auditable default 0, - b_audit_visibility nvarchar(32) null, -- 审计可见性(内部/客户可见) - b_sensitive tinyint not null -- 是否敏感字段(涉及脱敏/导出控制) - constraint df_s_module_field_sensitive default 0, - b_max_length int null, -- 字符串最大长度 - b_numeric_precision tinyint null, -- 数值精度(总位数) - b_numeric_scale tinyint null, -- 数值小数位 - b_temporal_type nvarchar(32) null, -- 时间类型(如 date/datetime) - b_unit_key nvarchar(128) null, -- 计量单位 key - b_default_value_text nvarchar(max) null, -- 默认值(文本表达) - b_aliases_text nvarchar(max) null, -- 字段别名(搜索/AI 理解用) - b_validation_text nvarchar(max) null, -- 校验规则(文本表达) - b_ai_hints_text nvarchar(max) null, -- 给 AI 理解的提示信息 - b_origin nvarchar(32) not null -- 来源:platform / tenant - constraint df_s_module_field_origin default N'platform', - b_required_by_code tinyint not null -- 是否被代码强依赖 - constraint df_s_module_field_required_by_code default 0, - b_created_at datetime2(3) not null -- 创建时间(UTC) - constraint df_s_module_field_created_at default sysutcdatetime(), - b_created_by nvarchar(128) not null, -- 创建人 - constraint pk_s_module_field primary key (b_id), - constraint ux_s_module_field_key unique ( - b_entity_key, - b_field_key - ) +create table s_field ( + b_id bigint identity(1, 1) not null, -- 内部主键 + b_entity_id bigint not null, -- 所属实体 + b_field varchar(50) not null, -- 物理列名(与真实表列一致) + b_name nvarchar(100) not null, -- 字段名称 + b_i18n varchar(150) null, -- 显示名多语言 key + b_type varchar(30) not null, -- 存储类型:varchar/decimal/datetime/int/bit/text + b_length int null, -- 长度 + b_precision int null, -- 数值精度(总位数) + b_scale int null, -- 数值小数位 + b_nullable int not null default 1, -- 数据库层是否可空 + b_default_value nvarchar(500) null, -- 默认值(文本表达) + b_options nvarchar(max) null, -- 下拉/单选选项(JSON 文本) + b_edittype varchar(30) null, -- 编辑组件:input/select/date/datetime/textarea/number/checkbox/radio + b_required int not null default 0, -- 编辑必填 + b_readonly int not null default 0, -- 编辑只读 + b_disabled int not null default 0, -- 编辑禁用 + b_listvisible int not null default 1, -- 是否显示在列表 + b_listwidth int null, -- 列表列宽(px) + b_queryable int not null default 0, -- 是否出现在查询条件 + b_querytype varchar(30) null, -- 查询组件:input/select/date + b_queryop varchar(20) null, -- 查询操作符:eq/like/gt/ge/lt/le/in/between + b_group_id bigint null, -- 所属分组(逻辑外键 s_field_group.b_id) + b_xh int not null default 0, -- 列表/表单排序 + b_canuse int not null default 1, -- 是否启用(停用隐藏并保留数据) + b_inputuser_id bigint null, + b_inputdatetime datetime2 null default sysdatetime(), + b_updateuser_id bigint null, + b_updatedatetime datetime2 null, + constraint pk_s_field primary key (b_id), + constraint ux_s_field_entity_field unique (b_entity_id, b_field), + constraint fk_s_field_entity foreign key (b_entity_id) references s_entity (b_id) ); -create index ix_s_module_field_entity_key - on dbo.s_module_field (b_entity_key, b_enabled, b_sort_no, b_field_key); - -create index ix_s_module_field_physical_column_name - on dbo.s_module_field (b_entity_key, b_physical_column_name); +create index ix_s_field_group on s_field (b_group_id, b_xh, b_canuse); ``` -写入模式取值(`b_write_mode`): +写入模式由 `b_readonly` 与 `b_canuse` 组合表达;如需更细的写入语义(computed/actionOnly/system),可增加 `b_writemode varchar(20)` 列(direct/createOnly/readOnly/computed/actionOnly/system)。 -```text -direct 可通过 saveobjt 普通保存(如备注、ETD、件数) -createOnly 仅创建时允许写入(如来源系统、初始业务类型) -readOnly 前端和普通接口只读(如汇总金额、关联展示值) -computed 由公式或计算服务生成(如毛利、计费重量) -actionOnly 只能通过指定业务动作修改(如审核状态、确认时间) -system 仅平台内部维护(如创建人、版本、流水状态) -``` +### 3.5 s_field_group 字段分组 -### 2.5 关系 - -`b_relation` 描述实体之间的引用与主子关系,通用运行时据此执行外键校验、主子事务、级联删除和子表区域渲染。 +分组是实体级独立定义,字段通过 `b_group_id` 引用,支持分组树。 ```sql -create table dbo.s_module_relation ( - b_id bigint identity(1, 1) not null, -- 内部自增主键 - b_relation_key nvarchar(192) not null, -- 关系稳定业务标识 - b_entity_key nvarchar(192) not null, -- 主实体 key - b_relation_type nvarchar(32) not null, -- 关系类型:one-to-one/one-to-many/many-to-one/many-to-many - b_target_entity_key nvarchar(192) not null, -- 目标实体 key - b_ownership nvarchar(32) not null -- 所有权(默认 reference 引用) - constraint df_s_module_relation_ownership default N'reference', - b_required tinyint not null -- 是否必填 - constraint df_s_module_relation_required default 0, - b_delete_policy nvarchar(32) null, -- 删除策略:restrict/set-null/cascade/archive - b_save_strategy nvarchar(32) null, -- 保存策略:separate 独立保存 / with-parent 随主实体事务保存 - b_order_field nvarchar(256) null, -- 子表排序字段表达式 - b_unique_constraint tinyint not null -- 是否唯一约束 - constraint df_s_module_relation_unique_constraint default 0, - b_name_key nvarchar(256) not null, -- 关系显示名多语言 key - b_enabled tinyint not null -- 是否启用 - constraint df_s_module_relation_enabled default 1, - b_config_text nvarchar(max) null, -- 关系扩展配置 - b_created_at datetime2(3) not null -- 创建时间(UTC) - constraint df_s_module_relation_created_at default sysutcdatetime(), - b_created_by nvarchar(128) not null, -- 创建人 - constraint pk_s_module_relation primary key (b_id), - constraint ux_s_module_relation_key unique (b_relation_key) +create table s_field_group ( + b_id bigint identity(1, 1) not null, -- 内部主键 + b_entity_id bigint not null, -- 所属实体 + b_parent_id bigint null, -- 父分组(分组树) + b_title nvarchar(100) not null, -- 分组标题 + b_i18n varchar(150) null, -- 标题多语言 key + b_xh int not null default 0, -- 分组排序 + b_canuse int not null default 1, -- 是否启用(停用整组隐藏) + constraint pk_s_field_group primary key (b_id), + constraint fk_s_field_group_entity foreign key (b_entity_id) references s_entity (b_id), + constraint fk_s_field_group_parent foreign key (b_parent_id) references s_field_group (b_id) ); -create index ix_s_module_relation_entity_key - on dbo.s_module_relation (b_entity_key, b_enabled); - -create index ix_s_module_relation_target_entity_key - on dbo.s_module_relation (b_target_entity_key); +create index ix_s_field_group_entity on s_field_group (b_entity_id, b_xh, b_canuse); ``` -### 2.6 页面菜单入口 +### 3.6 s_relation 实体关系 -每条菜单记录同时代表一个导航节点和一个页面入口。`directory` 仅表示目录节点,`page` 使用固定 Vue 页面组件,`data_category` 表示数据目录,`data` 表示由通用列表与表单组件维护的数据模块,`external` 表示外链。 +描述实体间的引用与主子关系,运行时据此执行外键校验、主子事务与级联删除。 ```sql -create table dbo.s_module_menu ( - b_id bigint identity(1, 1) not null, -- 内部自增主键 - b_menu_key nvarchar(192) not null, -- 菜单/页面稳定业务标识 - b_module_key nvarchar(128) null, -- 所属平台模块 key - b_parent_menu_key nvarchar(192) null, -- 父菜单 key(导航树自引用) - b_name_key nvarchar(256) not null, -- 菜单显示名多语言 key - b_menu_type nvarchar(32) not null, -- 菜单类型:directory/page/data_category/data/external - b_route nvarchar(256) null, -- 固定页面或外链路由 - b_entity_key nvarchar(192) null, -- data 类型绑定的实体 key - b_permission_key nvarchar(192) null, -- 页面访问权限 key - b_icon_key nvarchar(128) null, -- 菜单图标 key - b_sort_no int not null -- 菜单排序号 - constraint df_s_module_menu_sort_no default 0, - b_can_menu tinyint not null -- 是否显示在菜单中 - constraint df_s_module_menu_can_menu default 1, - b_enabled tinyint not null -- 是否启用 - constraint df_s_module_menu_enabled default 1, - b_config_text nvarchar(max) null, -- 菜单/页面扩展配置 - b_created_at datetime2(3) not null -- 创建时间(UTC) - constraint df_s_module_menu_created_at default sysutcdatetime(), - b_created_by nvarchar(128) not null, -- 创建人 - constraint pk_s_module_menu primary key (b_id), - constraint ux_s_module_menu_key unique (b_menu_key) +create table s_relation ( + b_id bigint identity(1, 1) not null, -- 内部主键 + b_entity_id bigint not null, -- 主实体 + b_target_entity_id bigint not null, -- 目标实体 + b_relation_type varchar(20) not null, -- one-to-one/one-to-many/many-to-one/many-to-many + b_foreign_field varchar(50) null, -- 子表侧外键字段 + b_link_field varchar(50) null, -- 主实体侧关联字段 + b_ownership varchar(20) not null default 'reference', -- reference 引用/owned 主从 + b_delete_policy varchar(20) null, -- restrict/set-null/cascade/archive + b_orderfield varchar(500) null, -- 子表排序字段表达式 + b_canuse int not null default 1, -- 是否启用 + b_xh int not null default 0, -- 排序号 + constraint pk_s_relation primary key (b_id), + constraint fk_s_relation_entity foreign key (b_entity_id) references s_entity (b_id), + constraint fk_s_relation_target foreign key (b_target_entity_id) references s_entity (b_id) ); -create index ix_s_module_menu_module_key - on dbo.s_module_menu (b_module_key, b_enabled); - -create index ix_s_module_menu_parent_key - on dbo.s_module_menu (b_parent_menu_key, b_sort_no, b_enabled); - -create index ix_s_module_menu_route - on dbo.s_module_menu (b_route, b_enabled); - -create index ix_s_module_menu_entity_key - on dbo.s_module_menu (b_entity_key, b_enabled); +create index ix_s_relation_target on s_relation (b_target_entity_id); ``` -约束由元数据校验器执行:`directory` 节点不能填写业务表或外链路由;`page` 节点必须填写 `b_route`;`data` 节点必须填写 `b_entity_key`(查询走实体 `b_view_table`,保存走实体 `b_save_table`);`external` 节点必须填写 `b_route` 和 `b_permission_key`。固定页面组件负责布局,表中只保存实体引用和导航配置。目录仅在至少存在一个可访问子页面时显示。 +### 3.7 s_menu 菜单/页面 -### 2.7 字段分组 - -字段分组是实体级独立定义,用于表单/编辑页面按分组展示字段。分组定义集中在 `s_module_field_group`,字段通过 `s_module_field.b_group_key` 引用分组,避免在每个字段上重复维护分组标题与排序。 +纯导航树,通过 `b_module_id` 挂到模块做组织维度,通过 `b_entity_id` 引用实体;模块与菜单可互相独立演进(一个模块多个菜单入口、一个菜单也可属于无模块的公共区)。 ```sql -create table dbo.s_module_field_group ( - b_id bigint identity(1, 1) not null, -- 内部自增主键 - b_entity_key nvarchar(192) not null, -- 所属实体 key - b_group_key nvarchar(128) not null, -- 分组稳定标识(如 basic/transport/finance) - b_name_key nvarchar(256) not null, -- 分组标题多语言 key - b_sort_no int not null -- 分组排序号(0,1,2...) - constraint df_s_module_field_group_sort_no default 0, - b_enabled tinyint not null -- 是否启用(停用则整组隐藏) - constraint df_s_module_field_group_enabled default 1, - b_config_text nvarchar(max) null, -- 分组扩展配置 - b_created_at datetime2(3) not null -- 创建时间(UTC) - constraint df_s_module_field_group_created_at default sysutcdatetime(), - b_created_by nvarchar(128) not null, -- 创建人 - constraint pk_s_module_field_group primary key (b_id), - constraint ux_s_module_field_group_key unique ( - b_entity_key, - b_group_key - ) +create table s_menu ( + b_id bigint identity(1, 1) not null, -- 内部主键 + b_parent_id bigint null, -- 父菜单(导航树自引用) + b_module_id bigint null, -- 所属模块(菜单的组织维度,可空) + b_code varchar(50) not null, -- 菜单编码 + b_name nvarchar(100) not null, -- 菜单名称 + b_i18n varchar(150) null, -- 显示名多语言 key + b_menu_type varchar(20) not null, -- directory/page/data/external + b_route varchar(200) null, -- 固定页面路由或外链 + b_entity_id bigint null, -- data 页面绑定的实体 + b_power_code varchar(50) null, -- 页面访问权限编码(对应 s_power.b_code) + b_icon varchar(50) null, -- 图标 + b_canmenu int not null default 1, -- 是否显示在菜单 + b_xh int not null default 0, -- 排序号 + b_canuse int not null default 1, -- 是否启用 + constraint pk_s_menu primary key (b_id), + constraint ux_s_menu_code unique (b_code), + constraint fk_s_menu_parent foreign key (b_parent_id) references s_menu (b_id), + constraint fk_s_menu_module foreign key (b_module_id) references s_module (b_id), + constraint fk_s_menu_entity foreign key (b_entity_id) references s_entity (b_id) ); -create index ix_s_module_field_group_entity_key - on dbo.s_module_field_group (b_entity_key, b_enabled, b_sort_no); +create index ix_s_menu_parent on s_menu (b_parent_id, b_xh, b_canuse); +create index ix_s_menu_entity on s_menu (b_entity_id); ``` -表单渲染时按 `b_sort_no` 排序分组,组内字段按 `s_module_field.b_sort_no` 排序;`s_module_field.b_group_key` 为空的字段显示在"未分组"区。新增/调整/停用分组只改 `s_module_field_group`,不需要逐个修改字段。 +菜单类型约束由元数据校验器执行:`directory` 不填业务路由;`page` 必须填 `b_route`;`data` 必须填 `b_entity_id`;`external` 必须填 `b_route` 和 `b_power_code`。目录仅在存在至少一个可访问子页面时显示。 -### 2.8 多语言 +### 3.8 s_autocode 自动编码 -多语言是元数据运行时的一部分,统一解析模块、菜单、实体、字段、动作和主数据的显示名称。 +独立于模块,挂实体 + 目标字段,规则可复用。并发控制:应用层使用原子更新取号(`update ... set b_currentvalue = b_currentvalue + 1 output inserted.b_currentvalue`),避免自增竞争。 ```sql -create table dbo.s_module_i18n ( - b_id bigint identity(1, 1) not null, -- 内部自增主键 - b_resource_key nvarchar(256) not null, -- 多语言资源 key(如 nameKey) - b_locale nvarchar(32) not null, -- 语言区域(如 zh-CN/en-US) - b_resource_value nvarchar(max) not null, -- 该语言下的显示文本 - b_source_type nvarchar(32) not null -- 来源类型(默认 manual 人工 / ai AI 生成) - constraint df_s_module_i18n_source_type default N'manual', - b_resource_status nvarchar(32) not null -- 资源状态(默认 confirmed 已确认) - constraint df_s_module_i18n_resource_status default N'confirmed', - b_model_key nvarchar(128) null, -- 生成翻译的 AI 模型标识(追溯用) - b_created_at datetime2(3) not null -- 创建时间(UTC) - constraint df_s_module_i18n_created_at default sysutcdatetime(), - b_created_by nvarchar(128) not null, -- 创建人 - constraint pk_s_module_i18n primary key (b_id), - constraint ux_s_module_i18n_key_locale unique ( - b_resource_key, - b_locale - ) +create table s_autocode ( + b_id bigint identity(1, 1) not null, -- 内部主键 + b_entity_id bigint not null, -- 所属实体 + b_field varchar(50) not null, -- 目标字段(物理列名) + b_prefix nvarchar(100) not null default '', -- 前缀 + b_dateformat varchar(30) not null default 'yyyyMM', -- 日期格式 + b_separator nvarchar(10) not null default '-', -- 分隔符 + b_seqwidth int not null default 5, -- 流水位宽 + b_resettype varchar(10) not null default 'month', -- 重置周期:none/day/month/year + b_startvalue bigint not null default 1, -- 起始值 + b_currentperiod varchar(8) null, -- 当前周期 + b_currentvalue bigint not null default 0, -- 当前流水值 + b_canuse int not null default 1, -- 是否启用 + constraint pk_s_autocode primary key (b_id), + constraint ux_s_autocode_entity_field unique (b_entity_id, b_field), + constraint fk_s_autocode_entity foreign key (b_entity_id) references s_entity (b_id) ); - -create index ix_s_module_i18n_resource_key - on dbo.s_module_i18n (b_resource_key, b_locale); ``` -## 3. Schema Compiler - -元数据变更通过服务层校验后立即生效。Schema Compiler 根据 `s_module_entity` 的保存数据源(`b_save_table`)配置和 `s_module_field` 的物理字段配置生成所需的 `CREATE TABLE`、`ALTER TABLE`、索引和约束语句,并由应用服务直接执行;执行成功后提交元数据变更,失败则回滚本次操作并返回错误。查询数据源(`b_view_table`)为 SQL 视图时,由运维或售后在数据库中维护视图定义,Schema Compiler 不生成视图 DDL。 - -执行机制: - -- DDL 同步执行,单租户库内加锁,同一时间只允许一个结构变更任务;其他租户不受影响。 -- 每个变更步骤记录开始时间、结束时间、checksum 和执行结果;重启或网络中断后可以从最后一个已确认步骤继续执行。 -- DDL、数据转换和结构校验全部成功后,才提交元数据变更;提交失败保持原结构,进入可修复状态。 -- 变更审计统一写入现有审计日志;本阶段不建立配置变更集、版本快照或迁移任务表,如果未来需要异步执行、失败重试、断点恢复或版本回滚,再增加专用任务表。 -- 破坏性变更(删除字段、缩短长度、改变类型)采用 expand-contract:先新增兼容结构、双写或分批迁移数据、校验新旧数据、停止写入旧结构、经过保留期后归档或删除旧结构。 - -## 4. 统一权限中心 - -### 4.1 角色与用户角色关系 +### 3.9 s_role 角色 ```sql -create table dbo.s_auth_role ( - b_id bigint identity(1, 1) not null, -- 内部自增主键 - b_role_key nvarchar(128) not null, -- 角色稳定业务标识 - b_role_name nvarchar(256) not null, -- 角色名称 - b_description nvarchar(1000) null, -- 角色描述 - b_role_type nvarchar(32) not null -- 角色类型(默认 custom 自定义) - constraint df_s_auth_role_role_type default N'custom', - b_enabled tinyint not null -- 是否启用 - constraint df_s_auth_role_enabled default 1, - b_created_at datetime2(3) not null -- 创建时间(UTC) - constraint df_s_auth_role_created_at default sysutcdatetime(), - b_created_by nvarchar(128) not null, -- 创建人 - b_updated_at datetime2(3) null, -- 更新时间 - b_updated_by nvarchar(128) null, -- 更新人 - constraint pk_s_auth_role primary key (b_id), - constraint ux_s_auth_role_key unique (b_role_key) +create table s_role ( + b_id bigint identity(1, 1) not null, -- 内部主键 + b_code varchar(50) not null, -- 角色编码 + b_name nvarchar(100) not null, -- 角色名称 + b_i18n varchar(150) null, -- 显示名多语言 key + b_remark nvarchar(500) null, -- 描述 + b_canuse int not null default 1, -- 是否启用 + b_inputuser_id bigint null, + b_inputdatetime datetime2 null default sysdatetime(), + b_updateuser_id bigint null, + b_updatedatetime datetime2 null, + constraint pk_s_role primary key (b_id), + constraint ux_s_role_code unique (b_code) ); - -create table dbo.s_auth_user_role ( - b_id bigint identity(1, 1) not null, -- 内部自增主键 - b_user_key nvarchar(128) not null, -- 用户稳定 key - b_role_key nvarchar(128) not null, -- 角色 key - b_enabled tinyint not null -- 是否启用(该用户角色关系) - constraint df_s_auth_user_role_enabled default 1, - b_effective_from datetime2(3) null, -- 生效开始时间(可定时) - b_effective_to datetime2(3) null, -- 生效结束时间(可定时) - b_created_at datetime2(3) not null -- 创建时间(UTC) - constraint df_s_auth_user_role_created_at default sysutcdatetime(), - b_created_by nvarchar(128) not null, -- 创建人 - constraint pk_s_auth_user_role primary key (b_id), - constraint ux_s_auth_user_role_key unique (b_user_key, b_role_key) -); - -create index ix_s_auth_user_role_role_key - on dbo.s_auth_user_role (b_role_key, b_enabled); ``` -### 4.2 功能权限 - -首期只做 RBAC 功能授权(页面、实体、动作、字段)。数据范围策略(`auth_data_policy`)和字段查看/导出/脱敏策略(`auth_field_policy`)作为后续扩展,不在本版本建表。 +### 3.10 s_user_role 用户-角色 ```sql -create table dbo.s_auth_permission ( - b_id bigint identity(1, 1) not null, -- 内部自增主键 - b_permission_key nvarchar(192) not null, -- 权限稳定业务标识 - b_module_key nvarchar(128) null, -- 所属模块 key - b_resource_type nvarchar(32) not null, -- 资源类型(如 menu/entity/field/action) - b_resource_key nvarchar(256) not null, -- 资源稳定 key(被授权的对象) - b_action_type nvarchar(64) null, -- 操作类型(如 view/create/update/delete/confirm) - b_name_key nvarchar(256) not null, -- 权限显示名多语言 key - b_description nvarchar(1000) null, -- 权限描述 - b_enabled tinyint not null -- 是否启用 - constraint df_s_auth_permission_enabled default 1, - b_created_at datetime2(3) not null -- 创建时间(UTC) - constraint df_s_auth_permission_created_at default sysutcdatetime(), - b_created_by nvarchar(128) not null, -- 创建人 - constraint pk_s_auth_permission primary key (b_id), - constraint ux_s_auth_permission_key unique (b_permission_key) +create table s_user_role ( + b_id bigint identity(1, 1) not null, -- 内部主键 + b_user_id bigint not null, -- 用户(逻辑外键,当前身份系统) + b_role_id bigint not null, -- 角色 + b_canuse int not null default 1, -- 是否启用该关系 + b_inputdatetime datetime2 null default sysdatetime(), + constraint pk_s_user_role primary key (b_id), + constraint ux_s_user_role unique (b_user_id, b_role_id), + constraint fk_s_user_role_role foreign key (b_role_id) references s_role (b_id) ); -create table dbo.s_auth_role_permission ( - b_id bigint identity(1, 1) not null, -- 内部自增主键 - b_role_key nvarchar(128) not null, -- 角色 key - b_permission_key nvarchar(192) not null, -- 权限 key - b_permission_effect nvarchar(16) not null -- 权限效果(默认 allow 允许) - constraint df_s_auth_role_permission_effect default N'allow', - b_created_at datetime2(3) not null -- 创建时间(UTC) - constraint df_s_auth_role_permission_created_at default sysutcdatetime(), - b_created_by nvarchar(128) not null, -- 创建人 - constraint pk_s_auth_role_permission primary key (b_id), - constraint ux_s_auth_role_permission_key unique ( - b_role_key, - b_permission_key - ) -); - -create index ix_s_auth_permission_resource_key - on dbo.s_auth_permission (b_resource_type, b_resource_key, b_enabled); - -create index ix_s_auth_role_permission_permission_key - on dbo.s_auth_role_permission (b_permission_key, b_permission_effect); +create index ix_s_user_role_role on s_user_role (b_role_id, b_canuse); ``` -菜单显示继承页面访问权限:用户拥有 `s_module_menu.b_permission_key` 对应权限时菜单才显示;目录菜单在无可访问子页面时自动隐藏。前端只控制显示,后端通用数据运行时和动作引擎必须再次检查同一个权限 key。 - -## 5. 日志与审计 - -### 5.1 技术日志 - -技术日志主要用于请求链路、SQL、异常、性能和外部接口排查,推荐写入 Loki、OpenSearch、Elasticsearch 等日志平台。下面的表适合集中日志库或短期数据库兜底,不建议把所有高频技术日志长期写入租户业务库。 +### 3.11 s_power 权限点 ```sql -create table dbo.s_log_technical ( - b_id bigint identity(1, 1) not null, -- 内部自增主键 - b_occurred_at datetime2(3) not null -- 发生时间(UTC) - constraint df_s_log_technical_occurred_at default sysutcdatetime(), - b_log_level nvarchar(16) not null, -- 日志级别(如 INFO/WARN/ERROR) - b_log_category nvarchar(64) not null, -- 日志分类(如 sql/exception/performance) - b_operation nvarchar(192) not null, -- 操作名(如 loadData/saveObject) - b_org_id nvarchar(128) null, -- 租户/组织 ID(日志来源标识) - b_user_key nvarchar(128) null, -- 操作用户 key - b_source_type nvarchar(32) null, -- 来源类型(如 web/api) - b_module_key nvarchar(128) null, -- 相关模块 key - b_operation_key nvarchar(192) null, -- 相关操作 key(如 saveObject.submit) - b_request_id nvarchar(128) null, -- 请求 ID - b_trace_id nvarchar(128) null, -- 追踪 ID(串联链路) - b_application_version nvarchar(128) null, -- 应用版本 - b_server_node nvarchar(128) null, -- 服务节点标识 +create table s_power ( + b_id bigint identity(1, 1) not null, -- 内部主键 + b_code varchar(50) not null, -- 权限编码(如 booking.confirm) + b_name nvarchar(100) not null, -- 权限名称 + b_i18n varchar(150) null, -- 显示名多语言 key + b_power_type varchar(20) not null, -- menu/entity/action/field + b_resource varchar(128) null, -- 资源标识(菜单 code/实体 code/动作 code/字段) + b_canuse int not null default 1, -- 是否启用 + b_xh int not null default 0, -- 排序号 + constraint pk_s_power primary key (b_id), + constraint ux_s_power_code unique (b_code) +); + +create index ix_s_power_resource on s_power (b_power_type, b_resource, b_canuse); +``` + +### 3.12 s_role_power 角色-权限 + +```sql +create table s_role_power ( + b_id bigint identity(1, 1) not null, -- 内部主键 + b_role_id bigint not null, -- 角色 + b_power_id bigint not null, -- 权限点 + b_canuse int not null default 1, -- 是否启用 + b_inputdatetime datetime2 null default sysdatetime(), + constraint pk_s_role_power primary key (b_id), + constraint ux_s_role_power unique (b_role_id, b_power_id), + constraint fk_s_role_power_role foreign key (b_role_id) references s_role (b_id), + constraint fk_s_role_power_power foreign key (b_power_id) references s_power (b_id) +); + +create index ix_s_role_power_power on s_role_power (b_power_id, b_canuse); +``` + +菜单显示继承页面访问权限:用户拥有 `s_menu.b_power_code` 对应权限时菜单才显示;前端只控制显示,后端运行时必须再次校验同一个权限编码。 + +### 3.13 s_i18n 多语言 + +```sql +create table s_i18n ( + b_id bigint identity(1, 1) not null, -- 内部主键 + b_key varchar(150) not null, -- 资源 key(如 booking.name) + b_locale varchar(20) not null, -- 语言区域(如 zh-CN/en-US) + b_value nvarchar(500) not null, -- 该语言下的显示文本 + b_i18ntype varchar(20) null, -- 资源类型:module/entity/field/menu/action + b_canuse int not null default 1, -- 是否启用 + constraint pk_s_i18n primary key (b_id), + constraint ux_s_i18n_key_locale unique (b_key, b_locale) +); + +create index ix_s_i18n_locale on s_i18n (b_locale, b_canuse); +``` + +### 3.14 s_log_technical 技术日志 + +用于请求链路、SQL、异常、性能与外部接口排查。高频日志推荐写入 Loki/OpenSearch/Elasticsearch 等日志平台,本表用于集中日志库或短期数据库兜底,不建议长期写入租户业务库。 + +```sql +create table s_log_technical ( + b_id bigint identity(1, 1) not null, -- 内部主键 + b_org_id varchar(50) null, -- 租户/组织标识(日志来源) + b_user_id bigint null, -- 操作用户(逻辑外键) + b_account varchar(50) null, -- 登录名快照 + b_sourcetype varchar(20) null, -- 来源类型:web/api + b_loglevel varchar(16) not null, -- 级别:INFO/WARN/ERROR + b_logcategory varchar(64) null, -- 分类:sql/exception/performance + b_operation varchar(100) null, -- 操作名(如 loadData/saveObject) + b_module_code varchar(50) null, -- 相关模块编码 + b_request_id varchar(64) null, -- 请求 ID + b_trace_id varchar(64) null, -- 追踪 ID(串联链路) + b_server_node varchar(128) null, -- 服务节点标识 b_duration_ms bigint null, -- 耗时(毫秒) - b_result_code nvarchar(128) null, -- 结果码 - b_error_code nvarchar(128) null, -- 错误码 + b_result_code varchar(50) null, -- 结果码 + b_error_code varchar(50) null, -- 错误码 b_error_message nvarchar(2000) null, -- 错误信息 b_exception_text nvarchar(max) null, -- 异常堆栈(文本) b_context_text nvarchar(max) null, -- 上下文信息(文本) + b_occurdatetime datetime2 null default sysdatetime(), -- 发生时间(UTC) constraint pk_s_log_technical primary key (b_id) ); -create index ix_s_log_technical_trace_id - on dbo.s_log_technical (b_trace_id, b_occurred_at desc); - -create index ix_s_log_technical_org_time - on dbo.s_log_technical (b_org_id, b_occurred_at desc); - -create index ix_s_log_technical_level_time - on dbo.s_log_technical (b_log_level, b_occurred_at desc); +create index ix_s_log_technical_trace on s_log_technical (b_trace_id, b_occurdatetime desc); +create index ix_s_log_technical_org on s_log_technical (b_org_id, b_occurdatetime desc); +create index ix_s_log_technical_level on s_log_technical (b_loglevel, b_occurdatetime desc); ``` -### 5.2 登录日志 +### 3.15 s_log_login 登录日志 -登录日志记录登录成功、失败、退出、刷新令牌和账号锁定等安全事件。它可以与技术日志使用相同的 `b_request_id`、`b_trace_id` 关联,但业务职责独立。 +记录登录成功、失败、退出、刷新令牌与账号锁定等安全事件,可通过 `b_request_id` / `b_trace_id` 与技术日志关联。 ```sql -create table dbo.s_log_login ( - b_id bigint identity(1, 1) not null, -- 内部自增主键 - b_org_id nvarchar(128) null, -- 租户/组织 ID - b_user_key nvarchar(128) null, -- 用户 key - b_login_name nvarchar(256) null, -- 登录名 - b_login_type nvarchar(32) not null, -- 登录类型(如 password/token) - b_login_status nvarchar(32) not null, -- 登录状态(如 success/failed) - b_auth_method nvarchar(32) null, -- 认证方式(如 ldap/oidc) - b_failure_reason nvarchar(512) null, -- 失败原因 - b_ip_address varchar(64) null, -- 登录 IP - b_user_agent nvarchar(1000) null, -- 客户端 UA - b_device_key nvarchar(256) null, -- 设备标识 - b_request_id nvarchar(128) null, -- 请求 ID - b_trace_id nvarchar(128) null, -- 追踪 ID - b_occurred_at datetime2(3) not null -- 发生时间(UTC) - constraint df_s_log_login_occurred_at default sysutcdatetime(), - b_created_at datetime2(3) not null -- 创建时间(UTC) - constraint df_s_log_login_created_at default sysutcdatetime(), +create table s_log_login ( + b_id bigint identity(1, 1) not null, -- 内部主键 + b_org_id varchar(50) null, -- 租户/组织标识 + b_user_id bigint null, -- 用户(逻辑外键) + b_account varchar(50) null, -- 登录名 + b_login_type varchar(20) not null, -- 登录类型:password/token/oidc/ldap + b_login_result int not null default 0, -- 登录结果:0失败/1成功 + b_fail_reason nvarchar(200) null, -- 失败原因 + b_ip varchar(50) null, -- 登录 IP + b_ip_location nvarchar(200) null, -- IP 归属地 + b_browser varchar(100) null, -- 浏览器 + b_browser_version varchar(50) null, -- 浏览器版本 + b_os varchar(100) null, -- 操作系统 + b_os_version varchar(50) null, -- 系统版本 + b_device_type varchar(20) null, -- 设备类型:pc/mobile/tablet + b_user_agent nvarchar(max) null, -- 完整 UA + b_session_id varchar(50) null, -- 会话 ID + b_request_id varchar(64) null, -- 请求 ID + b_trace_id varchar(64) null, -- 追踪 ID + b_occurdatetime datetime2 null default sysdatetime(), -- 登录时间(UTC) constraint pk_s_log_login primary key (b_id) ); -create index ix_s_log_login_user_time - on dbo.s_log_login (b_user_key, b_occurred_at desc); - -create index ix_s_log_login_name_time - on dbo.s_log_login (b_login_name, b_occurred_at desc); - -create index ix_s_log_login_status_time - on dbo.s_log_login (b_login_status, b_occurred_at desc); - -create index ix_s_log_login_trace_id - on dbo.s_log_login (b_trace_id, b_occurred_at desc); +create index ix_s_log_login_user on s_log_login (b_user_id, b_occurdatetime desc); +create index ix_s_log_login_account on s_log_login (b_account, b_occurdatetime desc); +create index ix_s_log_login_result on s_log_login (b_login_result, b_occurdatetime desc); ``` -### 5.3 业务审计事件与字段变更 +### 3.16 业务审计事件与字段变更 + +审计事件与字段变更分表存储:事件一条、字段变更多条,通过 `b_event_id` 关联。字段级明细是**面向客户的硬需求**(客户可在门户查看某个单号发生过哪些字段变更),**字段一旦发生修改即写入明细**,不按字段配置开关;`b_visibility` 控制内部/客户可见。 ```sql -create table dbo.s_log_audit ( - b_id bigint identity(1, 1) not null, -- 内部自增主键 - b_event_key nvarchar(192) not null, -- 业务事件稳定标识 - b_module_key nvarchar(128) not null, -- 被审计的数据模块 key - b_entity_id nvarchar(128) not null, -- 被审计的业务数据 ID - b_business_no nvarchar(192) null, -- 业务单号(如订舱号) - b_operation_key nvarchar(192) null, -- 触发的操作 key(如 saveObject.submit) - b_operator_key nvarchar(128) not null, -- 操作人 key - b_operator_name nvarchar(256) null, -- 操作人名称(快照) - b_source_type nvarchar(32) not null, -- 来源类型(如 user/ai/api) - b_visibility nvarchar(32) not null, -- 可见性(内部/客户可见) - b_request_id nvarchar(128) null, -- 请求 ID - b_trace_id nvarchar(128) not null, -- 追踪 ID(关联技术日志) - b_business_event_id bigint null, -- 关联的业务事件 ID +create table s_log_audit ( + b_id bigint identity(1, 1) not null, -- 内部主键 + b_event_code varchar(50) not null, -- 业务事件编码(如 booking.submit) + b_module_code varchar(50) null, -- 模块编码 + b_entity_code varchar(50) null, -- 实体编码 + b_data_id varchar(50) null, -- 被审计的业务数据 ID + b_business_no varchar(50) null, -- 业务单号(如订舱号) + b_operation varchar(100) null, -- 操作 key(如 saveObject.submit) + b_operator_id bigint null, -- 操作人(逻辑外键) + b_operator_name nvarchar(100) null, -- 操作人名称(快照) + b_source_type varchar(20) not null default 'user', -- 来源类型:user/ai/api + b_visibility varchar(20) not null default 'internal', -- 可见性:internal 内部/customer 客户可见 + b_request_id varchar(64) null, -- 请求 ID + b_trace_id varchar(64) null, -- 追踪 ID(关联技术日志) b_payload_text nvarchar(max) null, -- 事件载荷(文本,如前后快照) - b_occurred_at datetime2(3) not null -- 发生时间(UTC) - constraint df_s_log_audit_occurred_at default sysutcdatetime(), + b_occurdatetime datetime2 null default sysdatetime(), -- 发生时间(UTC) constraint pk_s_log_audit primary key (b_id) ); -create table dbo.s_log_audit_field ( - b_id bigint identity(1, 1) not null, -- 内部自增主键 - b_event_id bigint not null, -- 所属审计事件 ID - b_module_key nvarchar(128) not null, -- 数据模块 key - b_entity_id nvarchar(128) not null, -- 业务数据 ID - b_field_key nvarchar(128) not null, -- 变更的字段 key - b_field_label_snapshot nvarchar(256) null, -- 字段标签快照 - b_before_value_text nvarchar(max) null, -- 变更前原始值 - b_after_value_text nvarchar(max) null, -- 变更后原始值 - b_before_display_value nvarchar(1000) null, -- 变更前显示值(如引用字段 label) - b_after_display_value nvarchar(1000) null, -- 变更后显示值 - b_visibility nvarchar(32) not null, -- 可见性(内部/客户可见) - constraint pk_s_log_audit_field primary key (b_id) -); - -create index ix_s_log_audit_module - on dbo.s_log_audit (b_module_key, b_entity_id, b_occurred_at desc); - -create index ix_s_log_audit_trace_id - on dbo.s_log_audit (b_trace_id); - -create index ix_s_log_audit_field_audit_event_id - on dbo.s_log_audit_field (b_event_id); +create index ix_s_log_audit_entity on s_log_audit (b_entity_code, b_data_id, b_occurdatetime desc); +create index ix_s_log_audit_trace on s_log_audit (b_trace_id); ``` -## 6. 逻辑外键清单 +字段级变更明细表 `s_log_audit_field`:`b_event_id` 关联审计事件;客户门户查询按单号 + 客户可见过滤。 -以下关联不创建物理外键,由服务层、变更校验器和 Schema Compiler 负责检查: +```sql +create table s_log_audit_field ( + b_id bigint identity(1, 1) not null, -- 内部主键 + b_event_id bigint not null, -- 所属审计事件 + b_entity_code varchar(50) null, -- 实体编码 + b_data_id varchar(50) null, -- 业务数据 ID + b_field varchar(50) not null, -- 变更字段(物理列名) + b_field_label nvarchar(100) null, -- 字段标签快照 + b_before_value nvarchar(max) null, -- 变更前原始值 + b_after_value nvarchar(max) null, -- 变更后原始值 + b_before_display nvarchar(500) null, -- 变更前显示值(如引用字段 label) + b_after_display nvarchar(500) null, -- 变更后显示值 + b_visibility varchar(20) not null default 'internal', -- 可见性:internal 内部/customer 客户可见 + constraint pk_s_log_audit_field primary key (b_id), + constraint fk_s_log_audit_field_event foreign key (b_event_id) references s_log_audit (b_id) +); + +create index ix_s_log_audit_field_event on s_log_audit_field (b_event_id); +create index ix_s_log_audit_field_customer on s_log_audit_field (b_entity_code, b_data_id, b_visibility, b_event_id); +``` + +## 4. 关键设计决策说明 + +### 4.1 为什么模块与实体分离 +旧系统把表、视图、路由、菜单、权限全部压在 `s_module` 一条记录上,导致"一个模块只能配一张主表"、菜单与数据源互相绑架。本方案中模块只回答"这是什么业务能力、依赖谁",实体回答"数据在哪、怎么存",两者可独立演进:一个模块可拥有多个实体(订舱主表 + 集装箱/费用/运输子表),一个实体也可被多个菜单入口引用。 + +### 4.2 为什么保留模块树 + 独立依赖 DAG +树形(`b_parent_id`)表达"业务分组归属"(基础资料 > 海运 > 订舱),是货代业务的真实组织诉求,且操作直观;但树无法表达"finance 依赖 master-data"这类跨分支依赖,所以依赖单独用 `s_module_dependency` 表达并强制 DAG。树归树、依赖归依赖,语义不再混淆。 + +### 4.3 为什么字段合并单表 +旧系统字段配置拆 4 张表,字段新增要同步维护 4 处、分组标题另存一份,配置漂移是常态。合并后字段的"定义 + 列表 + 表单 + 查询"行为集中在一行,增删改一处完成;代价是行较宽,但对元数据表(数量级远小于业务表)完全可接受。 + +### 4.4 为什么菜单独立且引用实体 +导航是展示层概念,数据是业务层概念。`s_menu` 引用 `b_entity_id` 而非模块,同一实体可在多个菜单入口出现(如"订舱列表"与"订舱审核"共用 `ocean_booking` 实体但绑定不同权限)。 + +### 4.5 为什么权限引入角色层 +旧系统 `b_user_power` 是用户直挂权限,用户一多授权矩阵爆炸。本方案 `s_role` + `s_user_role` + `s_role_power` 三层,权限点 `s_power` 支持到 menu/entity/action/field 四级粒度,为后续数据范围、字段脱敏策略留出扩展位。 + +### 4.6 自动编码的并发处理 +`b_currentvalue` 若采用"读-改-写"必然在并发下产生重号。方案要求应用层用原子自增语句取号(`update ... set b_currentvalue = b_currentvalue + 1 output inserted.b_currentvalue`),并把取号与业务保存放同一事务,确保单号不重不漏。 + +### 4.7 日志的写入去向 +三类日志职责不同、量级不同,不建议全部落租户业务库: +- 技术日志高频(每条请求、SQL、异常),推荐写 Loki/ES 等日志平台,`s_log_technical` 仅作集中日志库或短期兜底。 +- 登录日志与业务审计属于安全与合规数据,低频但必须长期留存,落库保存;审计字段明细在字段发生修改时全量写入,不做按字段开关,数据量按变更频率自然收敛。 +- 三类日志统一以 `b_request_id` / `b_trace_id` 串联,便于从业务审计反查技术日志。 + +## 5. 逻辑外键清单 + +元数据表已建物理外键;以下为业务侧或跨系统引用,由服务层校验: | 表 | 字段 | 逻辑目标 | | --- | --- | --- | -| `s_module_dependency` | `b_module_key` | `s_module.b_module_key` | -| `s_module_dependency` | `b_depends_on_module_key` | `s_module.b_module_key` | -| `s_module_entity` | `b_module_key` | `s_module.b_module_key` | -| `s_module_field` | `b_entity_key` | `s_module_entity.b_entity_key` | -| `s_module_field` | `b_group_key` | 实体范围内的 `s_module_field_group.b_group_key` | -| `s_module_field_group` | `b_entity_key` | `s_module_entity.b_entity_key` | -| `s_module_relation` | `b_entity_key` | `s_module_entity.b_entity_key` | -| `s_module_relation` | `b_target_entity_key` | `s_module_entity.b_entity_key` | -| `s_module_menu` | `b_module_key` | `s_module.b_module_key` | -| `s_module_menu` | `b_parent_menu_key` | `s_module_menu.b_menu_key` | -| `s_module_menu` | `b_entity_key` | `s_module_entity.b_entity_key` | -| `s_module_menu` | `b_permission_key` | `s_auth_permission.b_permission_key` | -| `s_auth_user_role` | `b_role_key` | `s_auth_role.b_role_key` | -| `s_auth_role_permission` | `b_permission_key` | `s_auth_permission.b_permission_key` | -| `s_log_audit_field` | `b_event_id` | `s_log_audit.b_id` | -| `s_log_technical` | `b_user_key` | 当前身份系统的用户 `b_user_key` | -| `s_log_login` | `b_user_key` | 当前身份系统的用户 `b_user_key` | +| `s_user_role` | `b_user_id` | 当前身份系统的用户 `b_id` | +| `s_menu` | `b_module_id` | `s_module.b_id` | +| `s_field` | `b_group_id` | `s_field_group.b_id` | +| `s_menu` | `b_power_code` | `s_power.b_code` | +| `s_entity` | `b_viewtable` / `b_savetable` | Schema Compiler 生成的物理表/视图 | +| `s_log_technical` | `b_user_id` | 当前身份系统的用户 `b_id` | +| `s_log_login` | `b_user_id` | 当前身份系统的用户 `b_id` | +| `s_log_audit` | `b_operator_id` | 当前身份系统的用户 `b_id` | 逻辑外键写入时应执行以下校验: ```text -稳定 key 是否存在 --> 目标资源是否启用 +引用是否已启用 +-> 目标资源是否存在且类型匹配 -> 跨模块引用是否合法(依赖图中可达) --> 字段、实体类型和分组配置是否匹配 -> 删除或停用前是否仍被其他资源引用 ``` -## 7. 实施建议 +## 6. 与旧系统的映射对照 -建议按以下顺序创建和启用这些表: - -```text -元数据基础表(模块/依赖/实体/字段/字段分组/关系) --> 页面菜单入口表 --> 多语言表 --> 权限表 --> 日志与审计表 -``` - -正式执行前还应补充: - -- 根据实际 SQL Server 2022 环境确认过滤索引和 `datetime2` 支持情况。 -- 根据现有用户表确定 `b_user_key`、`b_created_by` 和 `b_updated_by` 的实际数据类型。 -- 根据最终稳定 key 长度评估唯一索引的键长度。 -- 为高数据量审计表制定归档及清理策略。 -- 用独立测试数据库执行完整建表脚本,并验证重复安装和 Schema 迁移流程。 -- 选择一条真实业务纵向验证(例如"基础资料 + 海运订舱 + 确认动作"),验证售后新增字段、Schema Compiler 加列、生成列表/表单和 DDL 失败回滚的完整链路。 +| 旧系统 | 本方案 | 变化 | +| --- | --- | --- | +| `s_module`(含表/路由/菜单/权限) | `s_module` + `s_entity` + `s_menu` | 职责拆分 | +| `s_module_field` | `s_field` | 字段挂实体 | +| `s_module_field_edit` / `_list` / `_query` | 并入 `s_field` 标志列 | 四表合一 | +| `s_module_field_group` | `s_field_group` | 分组挂实体 | +| `s_module_auto_code` | `s_autocode` | 挂实体+字段,原子取号 | +| `s_module_power` + `b_user_power` | `s_power` + `s_role` + `s_user_role` + `s_role_power` | 引入角色层 | +| `b_i18n` | `s_i18n` | 增加资源类型维度 | +| `s_login_log` | `s_log_login` | 字段扩充(设备/UA/请求链路) | +| (无) | `s_module_dependency` | 新增依赖 DAG | +| (无) | `s_relation` | 新增实体关系 | +| (无) | `s_log_technical` | 新增技术日志(旧系统无) | +| (无) | `s_log_audit` + `s_log_audit_field` | 新增业务审计与字段变更(旧系统无) | diff --git a/code/fms/fms-vue/pnpm-lock.yaml b/code/fms/fms-vue/pnpm-lock.yaml index ea7b47fa..1d86da4f 100644 --- a/code/fms/fms-vue/pnpm-lock.yaml +++ b/code/fms/fms-vue/pnpm-lock.yaml @@ -988,8 +988,9 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true gopd@1.2.0: @@ -2400,7 +2401,7 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.2 - glob@10.4.5: + glob@10.5.0: dependencies: foreground-child: 3.3.1 jackspeak: 3.4.3 @@ -2481,7 +2482,7 @@ snapshots: dependencies: config-chain: 1.1.13 editorconfig: 1.0.7 - glob: 10.4.5 + glob: 10.5.0 js-cookie: 3.0.8 nopt: 7.2.1 diff --git a/code/fms/fms-vue/pnpm-workspace.yaml b/code/fms/fms-vue/pnpm-workspace.yaml index 620a7bb7..b4d7c864 100644 --- a/code/fms/fms-vue/pnpm-workspace.yaml +++ b/code/fms/fms-vue/pnpm-workspace.yaml @@ -1,2 +1,12 @@ allowBuilds: '@parcel/watcher': true +minimumReleaseAgeExclude: + - '@vitest/coverage-v8@4.1.11' + - '@vitest/expect@4.1.11' + - '@vitest/mocker@4.1.11' + - '@vitest/pretty-format@4.1.11' + - '@vitest/runner@4.1.11' + - '@vitest/snapshot@4.1.11' + - '@vitest/spy@4.1.11' + - '@vitest/utils@4.1.11' + - vitest@4.1.11 diff --git a/code/test-demo/AI_CONTEXT.md b/code/test-demo/AI_CONTEXT.md index 25f44066..b8e9e013 100644 --- a/code/test-demo/AI_CONTEXT.md +++ b/code/test-demo/AI_CONTEXT.md @@ -1,10 +1,10 @@ -# AI Context — 模块设计器原型 +# AI Context — FMS 新系统元数据配置 Demo ## 项目目标 -这是一个 **FMS 模块管理系统的"字段设计器"交互原型**,用 React 实现,用于验证设计思路和交互方案。 +这是一个 **FMS 新系统元数据配置演示**,用 React 实现,用于直观展示如何配置模块体系,对应《FMS新系统核心表结构设计.md》中的 17 张核心表。 -解决的问题:原有系统将字段的列表、编辑、查询配置分散在 4 个 Tab 中,操作碎片化。目标是把所有配置围绕"字段"聚合,所见即所得。 +命名风格参考旧系统:表名 `s_` 前缀、字段 `b_` 前缀 + 驼峰(如 `b_viewtable`、`b_inputdatetime`)。 ## 技术栈 @@ -13,70 +13,54 @@ | 框架 | React 19 + TypeScript 6 | | 构建 | Vite 8 | | 样式 | Tailwind CSS v4 (`@tailwindcss/vite`) | -| 拖拽 | `@dnd-kit/core` + `@dnd-kit/sortable` | | 包管理 | pnpm | ## 文件结构 ``` src/ -├── types/index.ts # 所有 TS 类型定义 + FieldTypeDef 常量 -├── data/moduleData.ts # Mock 数据 + 工厂函数 +├── types/index.ts # 元数据模型(对应 17 张表)+ 工具函数 +├── data/mock.ts # 演示数据:海运订舱 ocean 模块全链路示例 ├── components/ -│ ├── ModuleSettings.tsx # 顶部模块设置面板(编码/名称/编辑模式等) -│ ├── FormPreview.tsx # 左侧:表单预览(按分组排列,可拖拽排序) -│ ├── TablePreview.tsx # 左侧:表格预览(可拖拽列排序 + 列宽调整) -│ ├── FieldConfigList.tsx # 右侧:字段配置列表(L/E/Q 拨动开关 + 展开快捷编辑) -│ └── DataSourceDrawer.tsx # 下拉数据源配置抽屉(视图/静态选项) -├── App.tsx # 主布局 + 全局状态 -├── main.tsx # 入口 -└── index.css # Tailwind + 自定义 CSS(toggle、drawer 等) +│ ├── ui.tsx # 共享 UI 小组件(Card/Badge/Field/Input/Toggle/表格) +│ ├── ModulePanel.tsx # Tab1 模块:s_module 树 + 基本信息 + s_module_dependency DAG +│ ├── EntityPanel.tsx # Tab2 实体与字段:s_entity + s_field_group + s_field 合并单表 +│ ├── MenuPanel.tsx # Tab3 菜单页面:s_menu 导航树(挂模块 + 引用实体) +│ ├── AutoCodePanel.tsx # Tab4 编码与多语言:s_autocode + s_i18n +│ └── PermissionPanel.tsx # Tab5 权限:s_role + s_user_role + s_power + s_role_power +├── App.tsx # 主布局 + Tab + 全局状态 + 统计 +├── main.tsx # 入口 +└── index.css # Tailwind 入口 ``` -## 核心设计理念 +## 设计理念 -1. **两栏布局**:左侧实时预览(表单/表格切换),右侧字段配置列表 -2. **零点击高频操作**:列表/编辑/查询开关做成拨动开关(toggle),直接拨不用展开 -3. **内联快捷编辑**:点击字段行 → 下方展开标签/编码/类型/分组/列宽等基础配置 -4. **拖拽排序**:表单模式和表格模式都支持拖拽调整字段顺序 -5. **表格列宽调整**:表格预览下拖拽列头右边缘可调整列宽 -6. **数据源配置**:下拉字段才有的复杂配置,用右侧抽屉单独处理 +1. **四 Tab 对应四组表**:模块(模块+依赖)、实体与字段(实体+分组+字段)、菜单页面、编码与多语言 +2. **所有数据为前端内存状态**:`App.tsx` 统一持有 state,面板组件通过 onChange 回调修改,演示"配置后立即生效" +3. **字段配置合并单表**:旧系统 4 张配置表(field/edit/list/query)合并为 `s_field` 单表,行内展开编辑 +4. **模块树 vs 菜单树分离**:`s_module.b_parent_id` 只做业务分组,`s_menu.b_parent_id` 才是导航层级 +5. **开关直接可拨**:必填/列表显示/可查询/启用等布尔字段用 Toggle 拨动 -## 数据结构 +## 覆盖的表 -### ModuleField(核心) -```ts -{ - id: number, code: string, label: string, type: string, - groupId: string | null, order: number, - // 列表 - listOn: boolean, listW: number, listAlign: "left"|"center"|"right", - // 编辑 - editOn: boolean, editReq: boolean, editRO: boolean, - // 查询 - queryOn: boolean, queryOp: string, queryDef: string, - // 数据源(下拉字段专用) - ds: { type:"view"|"static", ... } | null -} +| Tab | 表 | +| --- | --- | +| 模块 | `s_module`、`s_module_dependency` | +| 实体与字段 | `s_entity`、`s_field_group`、`s_field` | +| 菜单页面 | `s_menu` | +| 编码与多语言 | `s_autocode`、`s_i18n` | +| 权限 | `s_role`、`s_user_role`、`s_power`、`s_role_power` | + +## 启动 + +```bash +pnpm install # 已安装 +pnpm dev # http://localhost:3000 ``` -### FieldGroup -```ts -{ id: string, name: string } -``` - -## 当前状态 - -- [ ] 基本功能:模块设置、表单预览、表格预览、字段配置列表、数据源抽屉 均已实现 -- [ ] 无 lint 错误 -- [ ] 表单预览可拖拽排序 -- [ ] 表格预览可拖拽列排序 + 列宽调整 -- [ ] 右侧 L/E/Q 拨动开关正常 -- [x] 安装依赖 `pnpm install` -- [x] 启动 `pnpm dev`(端口 3000) - ## 注意事项 -- `@dnd-kit/modifiers` 不可用,用本地 `Modifier` 函数替代了 `restrictToVerticalAxis` / `restrictToHorizontalAxis` -- 所有样式在 `src/index.css` 中,使用 class 名而非 Tailwind 原子类的地方(toggle、drawer、field-row 等)是因为这些需要状态驱动的复杂样式 -- 类型定义在 `src/types/index.ts`,修改字段结构时记得同步更新 `src/data/moduleData.ts` 中的 `makeField()` 工厂函数 +- 演示数据在 `src/data/mock.ts`,`resetAll` 会重置所有 state 到初始值 +- 菜单 `s_menu` 通过 `b_module_id` 挂模块(组织维度)、`b_entity_id` 引用实体,两者可独立演进 +- 权限演示:角色(左)→ 角色-权限授权矩阵(授权/拒绝/未授权三态)→ 角色-用户 → 权限点字典,覆盖 `s_role`/`s_user_role`/`s_power`/`s_role_power` 四表 +- 新表结构中的日志表(`s_log_technical`/`s_log_login`/`s_log_audit`/`s_log_audit_field`)未在 Demo 中展示,如需可扩展 Tab diff --git a/code/test-demo/index.html b/code/test-demo/index.html index a5b120a5..69d2dd5d 100644 --- a/code/test-demo/index.html +++ b/code/test-demo/index.html @@ -4,7 +4,7 @@ - 模块设计器 — FMS + FMS 新系统 · 元数据配置 Demo
diff --git a/code/test-demo/src/App.tsx b/code/test-demo/src/App.tsx index e21e11af..091c9156 100644 --- a/code/test-demo/src/App.tsx +++ b/code/test-demo/src/App.tsx @@ -1,218 +1,202 @@ -import { useState, useCallback, useRef } from "react"; -import type { ModuleField, FieldGroup, EditMode, FieldDataSource } from "./types"; -import { defaultFields, defaultGroups, makeField } from "./data/moduleData"; -import ModuleSettings from "./components/ModuleSettings"; -import FieldConfigList from "./components/FieldConfigList"; -import FormPreview from "./components/FormPreview"; -import TablePreview from "./components/TablePreview"; -import DataSourceDrawer from "./components/DataSourceDrawer"; - -type PreviewMode = "form" | "table"; +// FMS 新系统元数据配置 Demo —— 主布局 +// 主视图:统一元数据树(菜单 → 模块 → 实体 → 字段/分组/编码)全链路维护 +// 辅助 Tab:权限(RBAC) +import { useMemo, useState } from "react"; +import type { + Autocode, + Entity, + Field, + FieldGroup, + I18n, + Menu, + Module, + ModuleDependency, + Power, + Role, + RolePower, + UserRole, +} from "./types"; +import { + initialAutocodes, + initialDependencies, + initialEntities, + initialFieldGroups, + initialFields, + initialI18ns, + initialMenus, + initialModules, + initialPowers, + initialRolePowers, + initialRoles, + initialUserRoles, +} from "./data/mock"; +import MetaTree, { type TreeSelection } from "./components/MetaTree"; +import NodeEditor from "./components/NodeEditor"; +import PermissionPanel from "./components/PermissionPanel"; export default function App() { - const [fields, setFields] = useState(defaultFields); - const [groups, setGroups] = useState(defaultGroups); - const [editMode, setEditMode] = useState("form"); - const [previewMode, setPreviewMode] = useState("form"); - const [activeId, setActiveId] = useState(null); - const [dsFieldId, setDsFieldId] = useState(null); - const [savedTip, setSavedTip] = useState(false); - const nextGroupId = useRef(10); + const [view, setView] = useState<"tree" | "permission">("tree"); + const [selection, setSelection] = useState(null); + const [modules, setModules] = useState(initialModules); + const [dependencies, setDependencies] = useState(initialDependencies); + const [entities, setEntities] = useState(initialEntities); + const [fields, setFields] = useState(initialFields); + const [groups, setGroups] = useState(initialFieldGroups); + const [menus, setMenus] = useState(initialMenus); + const [autocodes, setAutocodes] = useState(initialAutocodes); + const [i18ns, setI18ns] = useState(initialI18ns); + const [roles, setRoles] = useState(initialRoles); + const [userRoles, setUserRoles] = useState(initialUserRoles); + const [powers, setPowers] = useState(initialPowers); + const [rolePowers, setRolePowers] = useState(initialRolePowers); + const [resetKey, setResetKey] = useState(0); - const updateField = useCallback( - (id: number, key: keyof ModuleField, value: ModuleField[keyof ModuleField]) => { - setFields((prev) => - prev.map((f) => (f.id === id ? ({ ...f, [key]: value } as ModuleField) : f)) - ); - }, - [] + const stats = useMemo( + () => [ + { label: "模块", value: modules.length, table: "s_module" }, + { label: "实体", value: entities.length, table: "s_entity" }, + { label: "字段", value: fields.length, table: "s_field" }, + { label: "菜单", value: menus.length, table: "s_menu" }, + { label: "编码", value: autocodes.length, table: "s_autocode" }, + { label: "角色", value: roles.length, table: "s_role" }, + { label: "权限", value: powers.length, table: "s_power" }, + { label: "多语言", value: i18ns.length, table: "s_i18n" }, + ], + [modules, entities, fields, menus, autocodes, roles, powers, i18ns], ); - const updateFieldList = useCallback( - (id: number, patch: Partial) => { - setFields((prev) => prev.map((f) => (f.id === id ? { ...f, ...patch } : f))); - }, - [] - ); - - const addField = useCallback( - (groupId: string) => { - const id = Date.now(); - setFields((prev) => [...prev, makeField(groupId)]); - setActiveId(id); - }, - [] - ); - - const deleteField = useCallback((id: number) => { - setFields((prev) => prev.filter((f) => f.id !== id)); - setActiveId((cur) => (cur === id ? null : cur)); - }, []); - - // 配置列表:上/下移 - const reorderFields = useCallback((id: number, dir: -1 | 1) => { - setFields((prev) => { - const sorted = [...prev].sort((a, b) => a.order - b.order); - const i = sorted.findIndex((f) => f.id === id); - const j = i + dir; - if (j < 0 || j >= sorted.length) return prev; - const t = sorted[i].order; - sorted[i].order = sorted[j].order; - sorted[j].order = t; - return [...sorted]; - }); - }, []); - - // 预览区:拖拽排序(按 id 重排并顺续编号) - const reorderByDrag = useCallback((activeId: number, overId: number) => { - setFields((prev) => { - const sorted = [...prev].sort((a, b) => a.order - b.order); - const from = sorted.findIndex((f) => f.id === activeId); - const to = sorted.findIndex((f) => f.id === overId); - if (from < 0 || to < 0) return prev; - const [moved] = sorted.splice(from, 1); - sorted.splice(to, 0, moved); - return sorted.map((f, i) => ({ ...f, order: (i + 1) * 10 })); - }); - }, []); - - const resizeField = useCallback( - (id: number, width: number) => updateField(id, "listW", width), - [updateField] - ); - - const renameGroup = useCallback((id: string, name: string) => { - setGroups((prev) => prev.map((g) => (g.id === id ? { ...g, name } : g))); - }, []); - - const addGroup = useCallback((name: string) => { - const id = `g${nextGroupId.current++}`; - setGroups((prev) => [...prev, { id, name }]); - return id; - }, []); - - const openDataSource = useCallback((fieldId: number) => setDsFieldId(fieldId), []); - - const dsField = - dsFieldId != null ? fields.find((f) => f.id === dsFieldId) ?? null : null; - - const readOnly = editMode === "none"; + const resetAll = () => { + setModules(initialModules); + setDependencies(initialDependencies); + setEntities(initialEntities); + setFields(initialFields); + setGroups(initialFieldGroups); + setMenus(initialMenus); + setAutocodes(initialAutocodes); + setI18ns(initialI18ns); + setRoles(initialRoles); + setUserRoles(initialUserRoles); + setPowers(initialPowers); + setRolePowers(initialRolePowers); + setSelection(null); + setResetKey((k) => k + 1); + }; return ( -
+
{/* 顶栏 */} -
-
- 🚢 -
-
海运主单 · 模块设计器
-
字段配置所见即所得
+
+
+
+

FMS 新系统 · 元数据配置 Demo

+

+ 统一树维护:菜单 → 模块 → 数据(实体) → 编码 · 字段/分组在实体面板内维护 · 对应 17 张表 · 命名风格 s_表 / b_字段 +

+
+
+
+ {stats.map((s) => ( +
+
{s.value}
+
{s.label}
+
+ ))} +
+
-
-
- - -
+ {/* 视图切换 */} +
+ +
-
- {/* 左栏:实时预览 */} -
-
- - {previewMode === "form" ? "表单预览" : "表格预览"} - {readOnly && ( - - 只读 - - )} - - 点击字段可在右侧配置 -
-
- {previewMode === "form" ? ( - {}} - readOnly={readOnly} - /> - ) : ( - - )} -
-
- - {/* 右栏:模块设置 + 字段配置 */} -
- -
- + {view === "tree" ? ( +
+ +
+ {selection ? ( + + ) : ( +
+ 请在左侧树中选择一个节点开始配置 +
+ 顺着导航维护:菜单 → 模块 → 数据(实体) → 编码 · 字段/分组在实体面板内维护 +
+
+ )} +
-
+ ) : ( + + )}
- setDsFieldId(null)} - onUpdate={(ds: FieldDataSource | null) => { - if (dsFieldId != null) updateField(dsFieldId, "ds", ds); - }} - /> - - {savedTip && ( -
- 已保存 ✔ -
- )} +
+ 演示环境:仅前端内存状态,改动不落库。生产环境由服务层校验后触发 Schema Compiler 同步 DDL。 +
); } diff --git a/code/test-demo/src/assets/hero.png b/code/test-demo/src/assets/hero.png deleted file mode 100644 index 02251f4b956c55af2d76fd0788124d7eee2b45eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13057 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 P00000NkvXXu0mjft=yBf diff --git a/code/test-demo/src/assets/react.svg b/code/test-demo/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/code/test-demo/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/code/test-demo/src/assets/vite.svg b/code/test-demo/src/assets/vite.svg deleted file mode 100644 index 5101b674..00000000 --- a/code/test-demo/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/code/test-demo/src/components/AutoCodePanel.tsx b/code/test-demo/src/components/AutoCodePanel.tsx new file mode 100644 index 00000000..55cf1cc9 --- /dev/null +++ b/code/test-demo/src/components/AutoCodePanel.tsx @@ -0,0 +1,267 @@ +// 自动编码 s_autocode + 多语言 s_i18n 配置 +import { useState } from "react"; +import type { Autocode, Entity, I18n } from "../types"; +import { Badge, Card, EmptyRow, Field, Input, Select, Td, Th } from "./ui"; + +interface Props { + autocodes: Autocode[]; + i18ns: I18n[]; + entities: Entity[]; + onChangeAutocodes: (a: Autocode[]) => void; + onChangeI18ns: (i: I18n[]) => void; +} + +const RESET_TYPES = [ + { v: "none", label: "none 不重置" }, + { v: "day", label: "day 按天" }, + { v: "month", label: "month 按月" }, + { v: "year", label: "year 按年" }, +] as const; + +export default function AutoCodePanel({ autocodes, i18ns, entities, onChangeAutocodes, onChangeI18ns }: Props) { + const [i18nFilter, setI18nFilter] = useState("all"); + + const addAutocode = () => { + const id = Math.max(0, ...autocodes.map((a) => a.b_id)) + 1; + onChangeAutocodes([ + ...autocodes, + { + b_id: id, + b_entity_id: entities[0]?.b_id ?? 0, + b_field: "b_ywno", + b_prefix: "BK", + b_dateformat: "yyyyMM", + b_separator: "-", + b_seqwidth: 5, + b_resettype: "month", + b_startvalue: 1, + b_currentperiod: null, + b_currentvalue: 0, + b_canuse: 1, + }, + ]); + }; + + const updateAutocode = (aid: number, patch: Partial) => { + onChangeAutocodes(autocodes.map((a) => (a.b_id === aid ? { ...a, ...patch } : a))); + }; + + const removeAutocode = (aid: number) => onChangeAutocodes(autocodes.filter((a) => a.b_id !== aid)); + + const addI18n = () => { + const id = Math.max(0, ...i18ns.map((i) => i.b_id)) + 1; + onChangeI18ns([...i18ns, { b_id: id, b_key: "new.key", b_locale: "zh-CN", b_value: "", b_i18ntype: "module", b_canuse: 1 }]); + }; + + const updateI18n = (iid: number, patch: Partial) => { + onChangeI18ns(i18ns.map((i) => (i.b_id === iid ? { ...i, ...patch } : i))); + }; + + const removeI18n = (iid: number) => onChangeI18ns(i18ns.filter((i) => i.b_id !== iid)); + + const filteredI18ns = i18nFilter === "all" ? i18ns : i18ns.filter((i) => i.b_i18ntype === i18nFilter); + + return ( +
+ {/* 自动编码 */} + + + 新增规则 + + } + > + + + + + + + + + + + + {autocodes.length === 0 && } + {autocodes.map((a) => ( + + + + + + + + ))} + +
实体目标字段规则当前流水操作
+ + {a.b_field} + + {a.b_prefix} + {a.b_separator} + {a.b_dateformat} + {a.b_separator} + {"0".repeat(a.b_seqwidth)} + + (重置:{a.b_resettype}) + + {a.b_currentperiod ?? "-"} / {a.b_currentvalue} + + +
+ {autocodes.length > 0 && ( +
+ 取号使用原子自增(update ... set b_currentvalue = b_currentvalue + 1 output inserted.b_currentvalue), + 与业务保存同一事务,避免并发重号。 +
+ )} +
+ + {/* 自动编码规则明细(选中第一项展开编辑) */} + {autocodes[0] && ( + +
+ + updateAutocode(autocodes[0].b_id, { b_prefix: e.target.value })} /> + + + updateAutocode(autocodes[0].b_id, { b_dateformat: e.target.value })} + /> + + + updateAutocode(autocodes[0].b_id, { b_separator: e.target.value })} + /> + + + updateAutocode(autocodes[0].b_id, { b_seqwidth: Number(e.target.value) })} + /> + + + + + + updateAutocode(autocodes[0].b_id, { b_startvalue: Number(e.target.value) })} + /> + + + updateAutocode(autocodes[0].b_id, { b_currentperiod: e.target.value || null })} + /> + + + updateAutocode(autocodes[0].b_id, { b_currentvalue: Number(e.target.value) })} + /> + +
+
+ 下一单号预览:{autocodes[0].b_prefix} + {autocodes[0].b_separator} + {autocodes[0].b_dateformat} + {autocodes[0].b_separator} + {String(autocodes[0].b_currentvalue + 1).padStart(autocodes[0].b_seqwidth, "0")} +
+
+ )} + + {/* 多语言 */} + + + 新增资源 + + } + className="lg:col-span-2" + > +
+ {["all", "module", "entity", "field", "menu"].map((t) => ( + + ))} +
+ + + + + + + + + + + + {filteredI18ns.length === 0 && } + {filteredI18ns.map((i) => ( + + + + + + + + ))} + +
b_key 资源 keyb_locale 语言b_value 文本b_i18ntype 类型操作
{i.b_key} + {i.b_locale} + + updateI18n(i.b_id, { b_value: e.target.value })} /> + + + + +
+

+ 资源 key 全局唯一(b_key + b_locale 唯一约束);模块/实体/字段/菜单通过 b_i18n 引用这里的 key。 +

+
+
+ ); +} diff --git a/code/test-demo/src/components/DataSourceDrawer.tsx b/code/test-demo/src/components/DataSourceDrawer.tsx deleted file mode 100644 index d3707e30..00000000 --- a/code/test-demo/src/components/DataSourceDrawer.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import type { ModuleField, FieldDataSource } from "../types"; - -interface Props { - field: ModuleField | null; - open: boolean; - onClose: () => void; - onUpdate: (ds: FieldDataSource | null) => void; -} - -export default function DataSourceDrawer({ field, open, onClose, onUpdate }: Props) { - if (!field) return null; - - const ds = field.ds; - - function switchType(type: "view" | "static" | "none") { - if (type === "none") { - onUpdate(null); - } else if (type === "view") { - onUpdate({ type: "view", view: "", vf: "b_id", lf: "b_name", ob: "b_xh ASC" }); - } else { - onUpdate({ type: "static", items: [{ v: "", l: "" }] }); - } - } - - return ( - <> -
-
-
-

下拉数据源

- -
- -
-
- {/* Type selector */} -
- - - -
- - {!ds &&

该字段当前无数据源

} - - {ds?.type === "view" && ( -
-
- - -
-
-
- - -
-
- - -
-
-
- - -
-
-
预览
-
1 → MAERSK
-
2 → MSC
-
3 → CMA CGM
-
-
- )} - - {ds?.type === "static" && ( -
- {ds.items?.map((it, i) => ( -
- - → - - -
- ))} - -
- )} -
-
- -
- -
-
- - ); -} diff --git a/code/test-demo/src/components/EntityPanel.tsx b/code/test-demo/src/components/EntityPanel.tsx new file mode 100644 index 00000000..9646f092 --- /dev/null +++ b/code/test-demo/src/components/EntityPanel.tsx @@ -0,0 +1,489 @@ +// 实体配置:s_entity(数据所有者)+ s_field(字段定义合并单表)+ s_field_group(分组) +import { useState } from "react"; +import type { Entity, Field as FieldMeta, FieldGroup, Module } from "../types"; +import { parseOptions } from "../types"; +import { Badge, Card, EmptyRow, Field, Input, Select, Td, Th, TextArea, Toggle } from "./ui"; + +interface Props { + modules: Module[]; + entities: Entity[]; + fields: FieldMeta[]; + groups: FieldGroup[]; + onChangeEntities: (e: Entity[]) => void; + onChangeFields: (f: FieldMeta[]) => void; + onChangeGroups: (g: FieldGroup[]) => void; +} + +const ENTITY_TYPES = [ + { v: "table", label: "table 简单实体" }, + { v: "view", label: "view 复杂列表(视图)" }, + { v: "query", label: "query 纯查询(只读)" }, +] as const; + +const DELETE_POLICIES = [ + { v: "restrict", label: "restrict 有引用拒绝删除" }, + { v: "lifecycle", label: "lifecycle 草稿可删,正式作废" }, + { v: "cascade", label: "cascade 随主表级联删除" }, + { v: "hard", label: "hard 校验后物理删除" }, +] as const; + +const EDIT_TYPES = ["input", "select", "date", "datetime", "textarea", "number", "checkbox", "radio"] as const; +const QUERY_OPS = ["eq", "like", "gt", "ge", "lt", "le", "in", "between"] as const; + +export default function EntityPanel({ + modules, + entities, + fields, + groups, + onChangeEntities, + onChangeFields, + onChangeGroups, +}: Props) { + const [selectedModuleId, setSelectedModuleId] = useState(2); // ocean + const [selectedEntityId, setSelectedEntityId] = useState(101); + const [expandedField, setExpandedField] = useState(null); + + const moduleEntities = entities.filter((e) => e.b_module_id === selectedModuleId); + const selectedEntity = entities.find((e) => e.b_id === selectedEntityId) ?? moduleEntities[0]; + + const entityFields = selectedEntity ? fields.filter((f) => f.b_entity_id === selectedEntity.b_id) : []; + const entityGroups = selectedEntity ? groups.filter((g) => g.b_entity_id === selectedEntity.b_id) : []; + + const updateEntity = (patch: Partial) => { + if (!selectedEntity) return; + onChangeEntities(entities.map((e) => (e.b_id === selectedEntity.b_id ? { ...e, ...patch } : e))); + }; + + const addEntity = () => { + const id = Math.max(0, ...entities.map((e) => e.b_id)) + 1; + const e: Entity = { + b_id: id, + b_module_id: selectedModuleId, + b_parent_id: null, + b_code: `new_entity_${id}`, + b_name: "新实体", + b_i18n: `entity.new_entity_${id}`, + b_viewtable: `new_entity_${id}`, + b_savetable: `new_entity_${id}`, + b_keyfield: "b_id", + b_entity_type: "table", + b_delete_policy: "lifecycle", + b_canuse: 1, + b_xh: moduleEntities.length + 1, + b_bz: null, + }; + onChangeEntities([...entities, e]); + setSelectedEntityId(id); + }; + + const addField = () => { + if (!selectedEntity) return; + const id = Math.max(0, ...fields.map((f) => f.b_id)) + 1; + const nf: FieldMeta = { + b_id: id, + b_entity_id: selectedEntity.b_id, + b_field: `b_new_field_${id}`, + b_name: "新字段", + b_i18n: `field.b_new_field_${id}`, + b_type: "varchar", + b_length: 50, + b_precision: null, + b_scale: null, + b_nullable: 1, + b_default_value: null, + b_options: null, + b_edittype: "input", + b_required: 0, + b_readonly: 0, + b_disabled: 0, + b_listvisible: 1, + b_listwidth: 100, + b_queryable: 0, + b_querytype: null, + b_queryop: null, + b_group_id: null, + b_xh: entityFields.length + 1, + b_canuse: 1, + }; + onChangeFields([...fields, nf]); + setExpandedField(id); + }; + + const addGroup = () => { + if (!selectedEntity) return; + const id = Math.max(0, ...groups.map((g) => g.b_id)) + 1; + onChangeGroups([ + ...groups, + { + b_id: id, + b_entity_id: selectedEntity.b_id, + b_parent_id: null, + b_title: "新分组", + b_i18n: `group.new_${id}`, + b_xh: entityGroups.length, + b_canuse: 1, + }, + ]); + }; + + const updateField = (fid: number, patch: Partial) => { + onChangeFields(fields.map((f) => (f.b_id === fid ? { ...f, ...patch } : f))); + }; + + return ( +
+ {/* 左:模块 -> 实体列表 */} + + + + +
+ {moduleEntities.map((e) => { + const active = e.b_id === selectedEntityId; + return ( + + ); + })} +
+ +
+ + {/* 右:实体详情 + 字段 */} + {selectedEntity ? ( +
+ {/* 实体基本信息 */} + +
+ + updateEntity({ b_code: e.target.value })} /> + + + updateEntity({ b_name: e.target.value })} /> + + + + + + updateEntity({ b_viewtable: e.target.value })} /> + + + updateEntity({ b_savetable: e.target.value || null })} + /> + + + updateEntity({ b_keyfield: e.target.value })} /> + + + + + + + + +
+ updateEntity({ b_canuse: v ? 1 : 0 })} /> + {selectedEntity.b_canuse === 1 ? "启用" : "停用"} +
+
+
+
+ +