-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- onUpdate('module', v)"
- @i18n-saved="onI18nSaved"
- />
-
- onUpdate('listConfig', v)"
- @groups-saved="onGroupsSaved"
- @sync-edit="(v) => onUpdate('editConfig', v)"
- @sync-query="(v) => onUpdate('queryConfig', v)"
- />
- onUpdate('editConfig', v)"
- @groups-saved="onGroupsSaved"
- @sync-list="(v) => onUpdate('listConfig', v)"
- @sync-query="(v) => onUpdate('queryConfig', v)"
- />
- onUpdate('queryConfig', v)"
- />
-
- onUpdate('powers', v)"
- @i18n-saved="onI18nSaved"
- />
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/code/fms/fms-vue/src/views/system/user/components/PermissionGrantEditor.vue b/code/fms/fms-vue/src/views/system/user/components/PermissionGrantEditor.vue
new file mode 100644
index 00000000..fe936cd5
--- /dev/null
+++ b/code/fms/fms-vue/src/views/system/user/components/PermissionGrantEditor.vue
@@ -0,0 +1,219 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ group.name }}
+
+
+
+
+
+
+
+
+
+ {{ group.name }}
+
+
+
+
+
+
+
+
diff --git a/code/fms/fms-vue/src/views/system/user/components/RoleManagerModal.vue b/code/fms/fms-vue/src/views/system/user/components/RoleManagerModal.vue
new file mode 100644
index 00000000..c6c06f96
--- /dev/null
+++ b/code/fms/fms-vue/src/views/system/user/components/RoleManagerModal.vue
@@ -0,0 +1,257 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/code/fms/fms-vue/src/views/system/user/composables/usePermissionAdministration.js b/code/fms/fms-vue/src/views/system/user/composables/usePermissionAdministration.js
new file mode 100644
index 00000000..871a006d
--- /dev/null
+++ b/code/fms/fms-vue/src/views/system/user/composables/usePermissionAdministration.js
@@ -0,0 +1,309 @@
+import { ref } from "vue";
+import { loadDataApi, nextIdApi, saveObjectApi } from "@/services/api";
+
+function idSet(rows, field) {
+ return new Set(rows.map((item) => String(item[field])));
+}
+
+function grantDescriptor(
+ table,
+ ownerField,
+ targetField,
+ ownerId,
+ selectedIds,
+ existingRows,
+ extra,
+) {
+ const selected = new Set(selectedIds.map(String));
+ const existing = idSet(existingRows, targetField);
+ return {
+ table,
+ ownerField,
+ targetField,
+ ownerId: String(ownerId),
+ extra,
+ inserts: [...selected].filter((id) => !existing.has(id)),
+ deletes: existingRows
+ .filter((row) => !selected.has(String(row[targetField])))
+ .map((row) => ({ b_id: row.b_id })),
+ };
+}
+
+async function saveGrantDescriptors(descriptors, leadingRequests = []) {
+ const insertCount = descriptors.reduce((total, item) => total + item.inserts.length, 0);
+ let ids = [];
+ if (insertCount) {
+ const response = await nextIdApi(insertCount);
+ ids = Array.isArray(response.data) ? response.data.map(String) : [String(response.data)];
+ }
+ let idIndex = 0;
+ const requests = [...leadingRequests];
+ for (const descriptor of descriptors) {
+ const inserts = descriptor.inserts.map((targetId) => ({
+ b_id: ids[idIndex++],
+ [descriptor.ownerField]: descriptor.ownerId,
+ [descriptor.targetField]: targetId,
+ ...(descriptor.extra || {}),
+ }));
+ if (inserts.length || descriptor.deletes.length) {
+ requests.push({
+ table: descriptor.table,
+ key_field: "b_id",
+ inserts,
+ deletes: descriptor.deletes,
+ });
+ }
+ }
+ if (requests.length) await saveObjectApi(requests);
+}
+
+export function usePermissionAdministration(operatorId) {
+ const roles = ref([]);
+ const functionNodes = ref([]);
+ const pageNodes = ref([]);
+ const actions = ref([]);
+ const selectedRoleId = ref("");
+ const roleFunctionIds = ref([]);
+ const rolePageIds = ref([]);
+ const roleActionIds = ref([]);
+ const selectedUserId = ref("");
+ const userRoleIds = ref([]);
+ const userFunctionIds = ref([]);
+ const userPageIds = ref([]);
+ const userActionIds = ref([]);
+ const roleRows = ref({ functions: [], pages: [], actions: [] });
+ const userRows = ref({ roles: [], functions: [], pages: [], actions: [] });
+ const loading = ref(false);
+ const saving = ref(false);
+
+ async function loadCatalogs() {
+ loading.value = true;
+ try {
+ const responses = await Promise.all([
+ loadDataApi("b_role", "", "b_xh ASC, b_id ASC"),
+ loadDataApi("v_s_function_node", "b_canuse = 1", "b_xh ASC, b_id ASC"),
+ loadDataApi(
+ "v_s_page_node",
+ "b_canuse = 1 AND b_permission_required = 1",
+ "b_page_module_id ASC, b_xh ASC, b_id ASC",
+ ),
+ loadDataApi("v_s_module_action", "b_canuse = 1", "b_module_id ASC, b_xh ASC, b_id ASC"),
+ ]);
+ roles.value = responses[0].data || [];
+ functionNodes.value = responses[1].data || [];
+ pageNodes.value = responses[2].data || [];
+ actions.value = responses[3].data || [];
+ } finally {
+ loading.value = false;
+ }
+ }
+
+ async function loadRole(roleId) {
+ selectedRoleId.value = String(roleId || "");
+ if (!selectedRoleId.value) return;
+ const responses = await Promise.all([
+ loadDataApi("b_role_function_node", `b_role_id = ${selectedRoleId.value}`),
+ loadDataApi("b_role_page_node", `b_role_id = ${selectedRoleId.value}`),
+ loadDataApi("b_role_action", `b_role_id = ${selectedRoleId.value}`),
+ ]);
+ roleRows.value = {
+ functions: responses[0].data || [],
+ pages: responses[1].data || [],
+ actions: responses[2].data || [],
+ };
+ roleFunctionIds.value = [...idSet(roleRows.value.functions, "b_function_node_id")];
+ rolePageIds.value = [...idSet(roleRows.value.pages, "b_page_node_id")];
+ roleActionIds.value = [...idSet(roleRows.value.actions, "b_action_id")];
+ }
+
+ async function createRole(values) {
+ const response = await nextIdApi();
+ const id = String(response.data);
+ await saveObjectApi([
+ {
+ table: "b_role",
+ key_field: "b_id",
+ inserts: [
+ {
+ b_id: id,
+ b_code: String(values.b_code || "").trim(),
+ b_name: String(values.b_name || "").trim(),
+ b_i18n: `meta.role.${id}`,
+ b_is_super: values.b_is_super ? 1 : 0,
+ b_canuse: 1,
+ b_xh: Number(values.b_xh) || 0,
+ },
+ ],
+ },
+ ]);
+ await loadCatalogs();
+ await loadRole(id);
+ }
+
+ async function saveRole(values) {
+ const roleId = selectedRoleId.value;
+ if (!roleId) return;
+ saving.value = true;
+ try {
+ await saveGrantDescriptors(
+ [
+ grantDescriptor(
+ "b_role_function_node",
+ "b_role_id",
+ "b_function_node_id",
+ roleId,
+ roleFunctionIds.value,
+ roleRows.value.functions,
+ ),
+ grantDescriptor(
+ "b_role_page_node",
+ "b_role_id",
+ "b_page_node_id",
+ roleId,
+ rolePageIds.value,
+ roleRows.value.pages,
+ ),
+ grantDescriptor(
+ "b_role_action",
+ "b_role_id",
+ "b_action_id",
+ roleId,
+ roleActionIds.value,
+ roleRows.value.actions,
+ ),
+ ],
+ [
+ {
+ table: "b_role",
+ key_field: "b_id",
+ updates: [
+ {
+ b_id: roleId,
+ b_name: String(values.b_name || "").trim(),
+ b_canuse: values.b_canuse ? 1 : 0,
+ b_xh: Number(values.b_xh) || 0,
+ },
+ ],
+ },
+ ],
+ );
+ await loadCatalogs();
+ await loadRole(roleId);
+ } finally {
+ saving.value = false;
+ }
+ }
+
+ async function removeRole(roleId) {
+ const role = roles.value.find((item) => String(item.b_id) === String(roleId));
+ if (Number(role?.b_is_super) === 1) throw new Error("超级管理员角色不能删除");
+ const userRoles = await loadDataApi("b_user_role", `b_role_id = ${roleId}`);
+ if ((userRoles.data || []).length) throw new Error("该角色仍分配给用户");
+ await loadRole(roleId);
+ const dataScopes = await loadDataApi("b_role_data_scope", `b_role_id = ${roleId}`);
+ await saveObjectApi([
+ { table: "b_role_function_node", key_field: "b_id", deletes: roleRows.value.functions },
+ { table: "b_role_page_node", key_field: "b_id", deletes: roleRows.value.pages },
+ { table: "b_role_action", key_field: "b_id", deletes: roleRows.value.actions },
+ { table: "b_role_data_scope", key_field: "b_id", deletes: dataScopes.data || [] },
+ { table: "b_role", key_field: "b_id", deletes: [{ b_id: String(roleId) }] },
+ ]);
+ selectedRoleId.value = "";
+ await loadCatalogs();
+ }
+
+ async function loadUser(userId) {
+ selectedUserId.value = String(userId || "");
+ const responses = await Promise.all([
+ loadDataApi("b_user_role", `b_user_id = ${selectedUserId.value}`),
+ loadDataApi("b_user_function_node", `b_user_id = ${selectedUserId.value}`),
+ loadDataApi("b_user_page_node", `b_user_id = ${selectedUserId.value}`),
+ loadDataApi("b_user_action", `b_user_id = ${selectedUserId.value}`),
+ ]);
+ userRows.value = {
+ roles: responses[0].data || [],
+ functions: responses[1].data || [],
+ pages: responses[2].data || [],
+ actions: responses[3].data || [],
+ };
+ userRoleIds.value = [...idSet(userRows.value.roles, "b_role_id")];
+ userFunctionIds.value = [...idSet(userRows.value.functions, "b_function_node_id")];
+ userPageIds.value = [...idSet(userRows.value.pages, "b_page_node_id")];
+ userActionIds.value = [...idSet(userRows.value.actions, "b_action_id")];
+ }
+
+ async function saveUserPermissions() {
+ const userId = selectedUserId.value;
+ if (!userId) return;
+ const extra = { b_inputuser_id: String(operatorId.value || operatorId) };
+ saving.value = true;
+ try {
+ await saveGrantDescriptors([
+ grantDescriptor(
+ "b_user_role",
+ "b_user_id",
+ "b_role_id",
+ userId,
+ userRoleIds.value,
+ userRows.value.roles,
+ extra,
+ ),
+ grantDescriptor(
+ "b_user_function_node",
+ "b_user_id",
+ "b_function_node_id",
+ userId,
+ userFunctionIds.value,
+ userRows.value.functions,
+ extra,
+ ),
+ grantDescriptor(
+ "b_user_page_node",
+ "b_user_id",
+ "b_page_node_id",
+ userId,
+ userPageIds.value,
+ userRows.value.pages,
+ extra,
+ ),
+ grantDescriptor(
+ "b_user_action",
+ "b_user_id",
+ "b_action_id",
+ userId,
+ userActionIds.value,
+ userRows.value.actions,
+ extra,
+ ),
+ ]);
+ await loadUser(userId);
+ } finally {
+ saving.value = false;
+ }
+ }
+
+ return {
+ roles,
+ functionNodes,
+ pageNodes,
+ actions,
+ selectedRoleId,
+ roleFunctionIds,
+ rolePageIds,
+ roleActionIds,
+ selectedUserId,
+ userRoleIds,
+ userFunctionIds,
+ userPageIds,
+ userActionIds,
+ loading,
+ saving,
+ loadCatalogs,
+ loadRole,
+ createRole,
+ saveRole,
+ removeRole,
+ loadUser,
+ saveUserPermissions,
+ };
+}
diff --git a/code/fms/fms-vue/src/views/system/user/index.vue b/code/fms/fms-vue/src/views/system/user/index.vue
index bcc3e7b6..5f188336 100644
--- a/code/fms/fms-vue/src/views/system/user/index.vue
+++ b/code/fms/fms-vue/src/views/system/user/index.vue
@@ -1,428 +1,124 @@
-
{ if (record?.action?.key === 'edit') handleEdit(record.record) }"
- >
+
-
-
- 新增用户
+
+ 角色维护
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
标志位
-
-
- 启用
-
-
- 销售员
-
-
- 单据主管
-
-
- 可处理事务
-
-
- 允许异地登录
-
-
- 可见客户
-
-
- 管理员
-
-
- 操作权限
-
-
- 审批权限
-
-
-
+
+
+ 所属角色
+
+
+
+ {{ role.b_name }}
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ group.b_name }}
- {{ group.children.length }}
-
-
-
-
-
-
-
-
-
- {{ selectedModule.b_name }}
- {{ selectedModule.b_code }}
-
-
-
-
-
-
-
- 勾选左侧「{{ selectedModule.b_name }}」以授权访问,再配置具体操作权限
-
-
-
-
- 请选择左侧菜单
-
-
-
-
-
+
+
+
@@ -442,207 +138,45 @@ async function handleSave() {
border-radius: 0;
}
-// ── Modal internal styles ──
-.demo-basic-info {
- padding-top: 4px;
+.user-permission-layout {
+ display: grid;
+ height: 100%;
+ min-height: 0;
+ grid-template-rows: auto minmax(0, 1fr);
+ gap: 14px;
}
-.demo-field-label {
+.role-section,
+.additive-section {
+ min-height: 0;
+}
+
+.role-section > strong,
+.additive-section > strong {
display: block;
- margin-bottom: 5px;
- font-size: 13px;
- color: #333;
+ margin-bottom: 8px;
}
-.demo-required {
- color: #ff4d4f;
-}
-
-.demo-flags-card {
- margin-top: 20px;
- padding-top: 16px;
- border-top: 1px solid #f0f0f0;
-}
-
-.demo-card-title {
- margin-bottom: 10px;
- font-size: 13px;
- font-weight: 600;
- color: #666;
-}
-
-// ── Menu permission: left tree + right list ──
-.perm-layout {
- display: flex;
- height: 440px;
- border: 1px solid #f0f0f0;
- border-radius: 6px;
- overflow: hidden;
-}
-
-// 左侧:菜单树
-.perm-tree {
- width: 240px;
- min-width: 240px;
- padding: 6px 0;
- border-right: 1px solid #f0f0f0;
- overflow-y: auto;
- background: #fafbfc;
-}
-
-.tree-group-hd {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 8px 12px;
- cursor: pointer;
- user-select: none;
- transition: background 0.1s;
-
- &:hover {
- background: #f0f1f3;
- }
-}
-
-.tree-group-ic {
- display: flex;
- align-items: center;
- color: #999;
-}
-
-.tree-group-label {
- flex: 1;
- font-size: 13px;
- font-weight: 600;
- color: #1a1a1a;
-}
-
-.tree-group-count {
- font-size: 11px;
- color: #bbb;
-}
-
-.tree-group-body {
- display: flex;
- flex-direction: column;
-}
-
-.tree-item {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 8px 12px 8px 30px;
- cursor: pointer;
- transition: background 0.1s;
-
- &:hover {
- background: #ebedf0;
- }
-
- &.is-active {
- background: #e6f0ff;
-
- &:hover {
- background: #dcebff;
- }
- }
-}
-
-.tree-item-name {
- font-size: 13px;
- color: #333;
- user-select: none;
-}
-
-// 右侧:操作权限列表
-.perm-detail {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- background: #fff;
-}
-
-.perm-detail-hd {
- display: flex;
- align-items: baseline;
- gap: 8px;
- padding: 14px 20px;
- border-bottom: 1px solid #f0f0f0;
-}
-
-.perm-detail-title {
- font-size: 15px;
- font-weight: 600;
- color: #1a1a1a;
-}
-
-.perm-detail-sub {
- font-size: 12px;
- color: #bbb;
-}
-
-.perm-detail-body {
- flex: 1;
- padding: 12px 20px;
- overflow-y: auto;
-}
-
-.perm-detail-all {
- display: flex;
- align-items: center;
- gap: 8px;
+.role-options {
+ max-height: 92px;
+ overflow: auto;
padding: 10px 12px;
- margin-bottom: 6px;
- border-radius: 6px;
- background: #f6f7f9;
- font-size: 13px;
- color: #333;
- cursor: pointer;
- user-select: none;
+ border: 1px solid var(--fms-border);
}
-.perm-pow-list {
+.role-options :deep(.ant-checkbox-group) {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 8px 12px;
+}
+
+.additive-section {
display: flex;
flex-direction: column;
}
-.perm-pow-item {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 11px 12px;
- border-bottom: 1px solid #f5f5f5;
- font-size: 13px;
- color: #333;
- cursor: pointer;
- user-select: none;
-
- &:last-child {
- border-bottom: 0;
- }
-
- &:hover {
- background: #fafbfc;
- }
-}
-
-.perm-detail-tip {
- margin-top: 8px;
- padding: 36px 16px;
- text-align: center;
- font-size: 13px;
- color: #999;
- background: #fafbfc;
- border-radius: 6px;
-}
-
-.perm-detail-empty {
+.user-grants {
flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 13px;
- color: #bbb;
+ min-height: 0;
}
diff --git a/code/fms/数据库迁移计划.md b/code/fms/数据库迁移计划.md
index 3b0c7274..a75abe16 100644
--- a/code/fms/数据库迁移计划.md
+++ b/code/fms/数据库迁移计划.md
@@ -1,5 +1,7 @@
# FMS 业务数据库结构迁移计划
+> 本文件记录业务表迁移历史。当前模块元数据架构和 `033`/`034` 切换顺序以 `新数据库结构.md` 为准;文中旧授权表名只描述切换前状态。
+
## 目标与现状
- 源库:`G3HY2025`,只读取系统目录和视图定义。
diff --git a/code/fms/文件组件设计方案.md b/code/fms/文件组件设计方案.md
index 5eff34ec..227d66f0 100644
--- a/code/fms/文件组件设计方案.md
+++ b/code/fms/文件组件设计方案.md
@@ -124,7 +124,7 @@ FileUpload.vue a-upload 包装(调用 fileService.upload)
- 三栏 flex 布局:左 `FileCategoryTree`(宽 ~220px,受 `showTree` 控制),中 `FileList`(flex:1),右预览区(宽 ~40% 或固定 ~480px,仅当选中文件时显示)。
- 选中文件(来自 `FileList` 的 `select` 事件)→ 右栏渲染 `FileViewer` 并传入预览 URL(`fileService.getContentUrl(subid)`)。
- 负责 `moduleCode` → `moduleId`(b_id) 解析:挂载时调 `loadModuleConfiguration` / 通用 `loaddata` 查 `s_module` 得到 `b_id`,下传 `FileList` 备用(上传时带回)。
-- 工具栏按钮(上传/下载/删除)按 `moduleCode` 查 `b_user_power` 控制显隐/禁用(前端权限,沿用现有 `permissions` store 模式)。
+- 工具栏按钮(上传/下载/删除)通过 `permissions.canAction(moduleIdOrCode, actionCode)` 读取有效操作授权并控制显隐。
**Props**
| 名称 | 类型 | 必填 | 默认 | 说明 |
diff --git a/code/fms/新数据库结构.md b/code/fms/新数据库结构.md
index 804d02c8..7a9651b3 100644
--- a/code/fms/新数据库结构.md
+++ b/code/fms/新数据库结构.md
@@ -1,344 +1,203 @@
-# FMS 新数据库结构
+# FMS 模块架构
-## 设计原则
+## 目标
-- `s_module` 是核心元数据对象,统一表示菜单目录、页面和数据模块。
-- 字段来源于数据模块配置的查询视图。系统不创建业务字段,只同步视图字段,并维护其展示、编辑、查询和权限配置。
-- 页面和业务逻辑不完全元数据化。海运等复杂业务页面由 Vue 页面显式组合多个数据模块,元数据只提供 Table、Form 和 Query 配置。
-- 字段基础定义、列表、编辑、查询四个职责分离;一个字段可以只参与其中任一场景。
-- 编辑规则与列表布局解耦:`s_module_field_edit` 是通用编辑配置,FmsForm 和 FmsTable 可按自身规则消费;`s_module_field_list` 只描述列表展示,不保存编辑状态。
-- 模块权限控制访问,操作权限控制按钮动作;操作权限必须依赖模块访问权限。
-- 模块元数据表沿用旧系统的 `s_module*` 命名;基础资料和用户授权表使用 `b_`,字段名继续使用 `b_`。全部 `b_id` 使用 `bigint` 雪花 ID,返回前端时序列化为字符串。
-- 布尔字段统一使用 `int`,`1` 表示是、`0` 表示否;`b_xh` 数值越小越靠前。
-- 当前按机构使用独立数据库,表中不保存机构编号。表名、视图名和字段名必须由后端白名单校验,不能直接拼接 SQL。
-
-## 整体关系
+模块架构只解决五类稳定职责:
```text
-s_module
- |-- parent_id --> s_module
- |-- 1:N --------> s_module_field
- |-- 0:1 --------> s_module_auto_code --> s_module_field
- |-- 1:N --------> s_module_field_group -- parent_id --> s_module_field_group
- |-- 1:N --------> s_module_power ---- 1:N ----> b_user_power
- `-- 1:N --------> b_user_module
-
-s_module_field
- |-- 0:N --------> s_module_field_list -- group_id --> s_module_field_group
- |-- 0:1 --------> s_module_field_edit -- group_id --> s_module_field_group
- `-- 0:1 --------> s_module_field_query
-
-s_module / s_module_field / s_module_field_list / s_module_field_group / s_module_power
- `-- b_i18n --> b_i18n.b_key
+Module 可复用模块资源
+FunctionNode 菜单目录和页面入口
+PageNode 页面内部编排
+Profile 数据模块的使用方式
+Capability 字段、列表、表单、查询、操作等能力
```
-## 模块
+业务表、业务数据和手写 Vue 页面不进入元数据模型。通用读写继续使用 `/data/loaddata`、`/data/page`、`/data/saveobjt`,不增加模块专用接口。
+
+## 设计规则
+
+- 所有主键使用不可变 `bigint` 雪花 ID,前端按字符串处理。
+- 技术编码创建后不可修改;显示名称可以修改。
+- `s_module` 只保存资源公共属性,类型专属字段进入子类型表。
+- 数据模块必须自动创建一个 `default` Profile。
+- 字段和操作属于 Module;列表、表单、查询、导入、导出属于 Profile。
+- 树结构使用明确父子外键和循环检测,不把树路径保存为 JSON。
+- 关系和授权使用稳定 ID,不依赖名称或技术编码。
+- 只有控件扩展参数、渲染器参数等不可预知属性使用 JSON。
+- 不保留旧模块元数据兼容表、双写或页面专用模型。
+
+## 资源模型
### `dbo.s_module`
-统一保存目录、页面和数据模块。
-
| 字段 | 类型 | 说明 |
| --- | --- | --- |
-| `b_id` | `bigint` | 不可变雪花 ID 主键 |
-| `b_code` | `varchar(50)` | 可修改的唯一模块编码,例如 `m_sea`、`d_container` |
-| `b_parent_id` | `bigint` | 上级模块 ID;根节点为空 |
-| `b_name` | `nvarchar(100)` | 默认中文名称 |
-| `b_i18n` | `varchar(150)` | 多语言键 |
-| `b_module_type` | `varchar(20)` | `directory`、`page` 或 `data` |
-| `b_route` | `varchar(200)` | 页面路由,例如 `/sea/list` |
-| `b_viewtable` | `varchar(128)` | 查询来源表或视图;数据模块字段从此处同步 |
-| `b_savetable` | `varchar(128)` | 保存目标表 |
-| `b_keyfield` | `varchar(50)` | 主键字段,例如 `b_id` |
-| `b_orderfield` | `varchar(500)` | 默认排序,例如 `b_xh ASC, b_id DESC` |
-| `b_canmenu` | `int` | 是否显示在菜单,默认 `0` |
-| `b_canuse` | `int` | 是否启用,默认 `1` |
-| `b_xh` | `int` | 同级显示顺序,默认 `0` |
-
-约束:
-
-- `b_parent_id` 外键引用 `s_module.b_id`;保存服务校验父节点不形成循环。
-- `b_code` 唯一;`b_route` 非空时唯一。
-- `directory` 不能配置 `b_route`、`b_viewtable` 或 `b_savetable`。
-- `page` 必须配置 `b_route`;其业务数据由页面组合的数据模块提供。
-- `data` 不能显示菜单且不能配置 `b_route`;必须配置 `b_viewtable`,保存场景再按需配置 `b_savetable` 与 `b_keyfield`。
-- 建议建立 `(b_parent_id, b_xh, b_id)` 索引。
-
-## 字段来源与同步
-
-### `dbo.s_module_field`
-
-保存字段的基础定义。字段来自数据模块的 `b_viewtable`,并以 `(b_module_id, b_field)` 作为稳定标识;列表、编辑和查询场景属性均由独立配置表维护。
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| `b_id` | `bigint` | 雪花 ID 主键 |
-| `b_module_id` | `bigint` | 所属数据模块 ID |
-| `b_field` | `varchar(50)` | 视图字段名 |
-| `b_name` | `nvarchar(100)` | 默认字段名称,由管理员维护 |
-| `b_i18n` | `varchar(150)` | 多语言键 |
-| `b_type` | `varchar(30)` | 字段控件类型:`input`、`textarea`、`number`、`money`、`date`、`datetime`、`checkbox` 或 `select` |
-| `b_default_value` | `nvarchar(500)` | 新增记录时的默认值 |
-| `b_options` | `nvarchar(max)` | 类型专属 JSON 配置,例如选择器来源、日期格式或金额精度 |
+| `b_id` | `bigint` | 不可变雪花 ID |
+| `b_code` | `varchar(50)` | 全局唯一、不可变技术编码 |
+| `b_name` | `nvarchar(100)` | 默认名称 |
+| `b_i18n` | `varchar(150)` | `meta.module.` |
+| `b_module_type` | `varchar(20)` | `page/data/report/dashboard/workflow` |
| `b_canuse` | `int` | 是否启用 |
-约束:
+每个 Module 必须且只能有一个对应子类型记录:
-- `b_module_id` 外键引用 `s_module.b_id`,`(b_module_id, b_field)` 唯一。
-- 建议建立 `(b_module_id, b_canuse, b_id)` 索引。
-- 字段没有 list、edit 或 query 配置记录时,不参与对应场景。
-- `b_type` 是展示和编辑的唯一类型来源:Table 按它渲染默认显示器,Form 或可编辑 Table 按它选择控件。
-- `select` 是否允许自由输入、选项来源、值字段和显示字段均由 `b_options` 控制。
-
-### `dbo.s_module_auto_code`
-
-每个数据模块最多绑定一条自动业务编号规则。`b_id` 仍是数据库技术主键,自动编码写入模块绑定的业务字段;业务字段自身应在实际保存表上建立唯一索引。
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| `b_id` | `bigint` | 雪花 ID 主键 |
-| `b_module_id` | `bigint` | 所属数据模块 ID,唯一 |
-| `b_field_id` | `bigint` | 写入业务编号的字段定义 ID |
-| `b_prefix` | `nvarchar(100)` | 固定前缀,可为空 |
-| `b_date_format` | `varchar(30)` | 日期格式,可为空 |
-| `b_separator` | `nvarchar(10)` | 编码各部分之间的分隔符,可为空 |
-| `b_sequence_width` | `int` | 流水号补零位数 |
-| `b_reset_type` | `varchar(10)` | `none`、`year`、`month` 或 `day` |
-| `b_start_value` | `bigint` | 流水号起始值 |
-| `b_current_period` | `varchar(8)` | 当前计数周期 |
-| `b_current_value` | `bigint` | 当前已分配流水值 |
-| `b_canuse` | `int` | 是否启用 |
-
-取号时锁定规则行并递增 `b_current_value`。编号生成后不允许业务用户修改;取号与业务保存分开时允许断号,但目标业务字段必须建立唯一索引。
-
-### 视图同步规则
-
-字段同步服务仅面向 `b_module_type = 'data'` 且配置了 `b_viewtable` 的模块执行:
-
-1. 后端校验视图名并读取列名、SQL 类型和可空性等结构信息。
-2. 按列名查找同模块的 `s_module_field`;新列创建字段基础记录。
-3. 新字段按 SQL 类型初始化 `b_type`,例如字符串映射为 `input`、长文本映射为 `textarea`、数值映射为 `number`、日期映射为 `date` 或 `datetime`、位值映射为 `checkbox`。已有字段不覆盖管理员维护的 `b_type` 与 `b_options`;`select`、`money` 等业务类型由管理员按语义调整。
-4. 本次视图中不存在的已有字段保留其记录和全部场景配置,但将 `b_canuse` 置为 `0`。
-5. 不覆盖人工维护的 `b_name`、`b_i18n`、`b_default_value` 与 `b_options`,不物理删除字段或场景配置。
-
-## 列表配置
-
-### `dbo.s_module_field_list`
-
-控制列表字段列。多级表头由 `s_module_field_group` 的 `list` 分组树表达;本表不保存编辑状态。
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| `b_id` | `bigint` | 雪花 ID 主键 |
-| `b_module_id` | `bigint` | 所属模块 ID |
-| `b_field_id` | `bigint` | 关联 `s_module_field.b_id` |
-| `b_group_id` | `bigint` | 所属分组 ID;为空时为顶层列 |
-| `b_i18n` | `varchar(150)` | 字段标题多语言键覆盖值 |
-| `b_visible` | `int` | 是否显示,默认 `1` |
-| `b_xh` | `int` | 同一分组内的显示顺序,默认 `0` |
-| `b_width` | `int` | 字段列宽 |
-
-约束:
-
-- `b_module_id` 外键引用 `s_module.b_id`,`b_field_id` 外键引用 `s_module_field.b_id`,`b_group_id` 引用同模块的 `s_module_field_group`。
-- `b_field_id` 必填;同一字段在同一模块的列表中最多出现一次,使用 `(b_module_id, b_field_id)` 唯一约束。
-- 字段列的显示器由关联字段的 `b_type` 与 `b_options` 决定,例如 `money`、`date`、`datetime` 均由前端按统一规则渲染。建议建立 `(b_module_id, b_group_id, b_xh, b_id)` 索引。
-- 不保存 `level`、`colspan`、`rowspan`;前端加载 Table 分组树和字段列后,按分组的 `b_parent_id`、`b_xh` 组装嵌套表头。隐藏分组的后代不显示;没有可见字段后代的分组不渲染。
-
-示例:
-
-```text
-费用(`s_module_field_group`)
-|-- 人工费(`s_module_field_list.b_group_id` 指向费用)
-`-- 海运费(`s_module_field_list.b_group_id` 指向费用)
-```
-
-## 编辑配置
-
-### `dbo.s_module_field_edit`
-
-控制字段的通用编辑属性、显示状态与编辑分组。控件类型和类型专属参数统一由 `s_module_field.b_type`、`b_options` 决定;FmsForm 与 FmsTable 自行决定何时使用本配置。
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| `b_id` | `bigint` | 雪花 ID 主键 |
-| `b_module_id` | `bigint` | 所属模块 ID |
-| `b_field_id` | `bigint` | 关联 `s_module_field.b_id` |
-| `b_required` | `int` | 是否必填,默认 `0` |
-| `b_readonly` | `int` | 是否只读,默认 `0` |
-| `b_disabled` | `int` | 是否禁用,默认 `0` |
-| `b_visible` | `int` | 是否显示,默认 `1` |
-| `b_group_id` | `bigint` | 所属编辑分组 ID;不分组时为空 |
-| `b_xh` | `int` | 编辑场景中的显示顺序,默认 `0` |
-
-约束:
-
-- `b_module_id` 外键引用 `s_module.b_id`,`b_field_id` 外键引用 `s_module_field.b_id`;`(b_module_id, b_field_id)` 唯一,保存服务校验字段属于同一模块。
-- `b_group_id` 必须关联同模块的 `s_module_field_group`;分组为空时字段进入默认编辑区域。
-- 已分组字段按分组的 `b_xh` 排列,组内按 `b_xh` 排列;默认区域字段按 `b_xh` 排列。
-
-## 布局分组
-
-### `dbo.s_module_field_group`
-
-统一保存列表多级表头和编辑分组。分组只负责布局,不定义编辑能力;列表或编辑用途由引用它的配置表决定。
-
-被 `s_module_field_list` 引用时,分组组成同一份 FmsTable 的多级表头;被 `s_module_field_edit` 引用时,分组组成同一份编辑界面中的业务区块,例如提单、订舱、报关、备注,不代表拆分为多个 Form。
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| `b_id` | `bigint` | 雪花 ID 主键 |
-| `b_module_id` | `bigint` | 所属模块 ID |
-| `b_parent_id` | `bigint` | 上级分组 ID;顶层为空 |
-| `b_title` | `nvarchar(100)` | 默认分组标题 |
-| `b_i18n` | `varchar(150)` | 分组标题多语言键 |
-| `b_xh` | `int` | 分组显示顺序,默认 `0` |
-| `b_canuse` | `int` | 是否启用,默认 `1` |
-
-约束:
-
-- `b_module_id` 外键引用 `s_module.b_id`;`b_id` 是分组的稳定关联键。
-- `b_parent_id` 必须引用同模块的分组且不能形成循环。
-- 多级分组可用于 FmsTable 表头;FmsForm 与 FmsTable 如何消费编辑分组由各自实现决定。
-- 建议建立 `(b_module_id, b_canuse, b_xh, b_id)` 索引。
-- 无字段引用的分组不渲染;分组停用时,其关联字段或列不显示。
-
-视图型选择器的 `b_options` 示例:
-
-```json
-{
- "source": "view",
- "view": "v_partner",
- "valueField": "b_id",
- "labelField": "b_name",
- "allowInput": false
-}
-```
-
-`select` 使用 `allowInput: false` 时保存 `valueField` 的值;设为 `true` 时可保存选择值或用户输入文本。视图名、值字段和标签字段均须由后端校验。
-
-## 查询配置
-
-### `dbo.s_module_field_query`
-
-独立控制主列表查询条件,不依赖字段是否配置为可见列表列。
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| `b_id` | `bigint` | 雪花 ID 主键 |
-| `b_module_id` | `bigint` | 所属模块 ID |
-| `b_field_id` | `bigint` | 关联 `s_module_field.b_id` |
-| `b_component` | `varchar(30)` | 查询组件 |
-| `b_operator` | `varchar(20)` | `=`、`like`、`between` 或 `in` |
-| `b_default_value` | `nvarchar(500)` | 默认查询值 |
-| `b_xh` | `int` | 查询条件显示顺序,默认 `0` |
-| `b_canuse` | `int` | 是否启用,默认 `1` |
-
-约束:
-
-- `b_module_id` 外键引用 `s_module.b_id`,`b_field_id` 外键引用 `s_module_field.b_id`;`(b_module_id, b_field_id)` 唯一,保存服务校验模块一致性。
-- 查询组件与操作符由后端白名单校验;查询值必须参数化,不能由配置直接拼接 SQL。
-- 建议建立 `(b_module_id, b_canuse, b_xh, b_id)` 索引。
-
-## 权限与多语言
-
-### `dbo.s_module_power`
-
-定义模块内可授权的按钮操作。
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| `b_id` | `bigint` | 雪花 ID 主键 |
-| `b_module_id` | `bigint` | 所属模块 ID |
-| `b_code` | `varchar(50)` | 操作编码,例如 `create`、`update`、`delete`、`audit`、`export` |
-| `b_name` | `nvarchar(100)` | 操作名称 |
-| `b_i18n` | `varchar(150)` | 多语言键 |
-| `b_xh` | `int` | 显示顺序 |
-| `b_canuse` | `int` | 是否启用,默认 `1` |
-
-约束:`b_module_id` 外键引用 `s_module.b_id`,`(b_module_id, b_code)` 唯一。
-
-### `dbo.b_user_module`
-
-控制用户能否查看、进入和调用模块。
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| `b_id` | `bigint` | 雪花 ID 主键 |
-| `b_user_id` | `bigint` | 用户雪花 ID |
-| `b_module_id` | `bigint` | 模块 ID |
-| `b_inputuser_id` | `bigint` | 授权人雪花 ID |
-| `b_inputdatetime` | `datetime` | 授权时间 |
-
-约束:`b_module_id` 外键引用 `s_module.b_id`,`(b_user_id, b_module_id)` 唯一。目录不单独授权;用户拥有下级模块时自动显示上级目录。
-
-### `dbo.b_user_power`
-
-控制用户可执行的按钮操作。
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| `b_id` | `bigint` | 雪花 ID 主键 |
-| `b_user_id` | `bigint` | 用户雪花 ID |
-| `b_module_id` | `bigint` | 所属模块 ID |
-| `b_power_id` | `bigint` | `s_module_power.b_id` |
-| `b_inputuser_id` | `bigint` | 授权人雪花 ID |
-| `b_inputdatetime` | `datetime` | 授权时间 |
-
-逻辑关联:`b_module_id` 对应 `s_module.b_id`,`b_power_id` 对应 `s_module_power.b_id`;`(b_user_id, b_power_id)` 唯一。建议建立 `(b_module_id, b_user_id)` 索引。
-
-权限规则:
-
-1. 用户必须拥有 `b_user_module` 记录,才能进入或调用该模块。
-2. 用户必须同时拥有模块权限和对应 `b_user_power` 记录,才能执行按钮动作。
-3. 前端据此控制路由进入和按钮可用性;后端接口后续实施同等权限校验。
-
-### `dbo.b_i18n`
-
-统一保存菜单、模块、字段、权限、按钮、提示和校验信息等界面文案。
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| `b_id` | `bigint` | 雪花 ID 主键 |
-| `b_key` | `varchar(150)` | 多语言键 |
-| `b_locale` | `varchar(20)` | 语言编码,例如 `zh-CN` |
-| `b_value` | `nvarchar(500)` | 翻译文本 |
-
-约束:`(b_key, b_locale)` 唯一。
-
-## 页面组合示例
-
-海运业务页面由 Vue 显式组合数据模块,不通过字段配置创建动态页面区域:
-
-```text
-海运页面 m_sea(page:路由、菜单、权限、页面编排)
-|-- 海运主单 d_sea(data)
-|-- 费用 d_charge(data)
-|-- 订舱 d_booking(data,独立表和主键时)
-`-- 箱明细 d_container(data)
-```
-
-若订舱字段实际保存于海运主单表,不创建 `d_booking`,而在 `d_sea` 的编辑配置中用 `b_group_id` 和 `b_xh` 定义编辑分组与顺序,分组标题来自 `s_module_field_group`。主子表关联、加载时机、保存顺序和页面布局均由业务 Vue 页面明确实现。
-
-## 现有字段迁移
-
-当前 `s_module_field` 混合保存基础、列表、编辑和查询属性。迁移应分阶段实施:
-
-| 当前字段 | 目标位置 |
+| 子类型表 | 主要字段 |
| --- | --- |
-| `b_type` | `s_module_field.b_type` |
-| `b_canlist` | `s_module_field_list.b_visible` |
-| `b_width` | `s_module_field_list.b_width` |
-| `b_xh` | 初始化 `s_module_field_list.b_xh`、`s_module_field_edit.b_xh` 与 `s_module_field_query.b_xh` |
-| `b_canform` | 为原 Form 字段创建 `s_module_field_edit` 记录 |
-| `b_must` | `s_module_field_edit.b_required` |
-| `b_readonly` | `s_module_field_edit.b_readonly` |
-| `b_canquery` | 创建 `s_module_field_query` 记录 |
-| `b_value` | 迁移为 `s_module_field.b_default_value` |
+| `s_module_page` | `b_route`、`b_page_mode`、`b_component_key` |
+| `s_module_data` | `b_viewtable`、`b_savetable`、`b_keyfield`、`b_orderfield` |
+| `s_module_report` | 来源数据模块/Profile、`b_renderer_key`、`b_options` |
+| `s_module_dashboard` | `b_component_key`、`b_options` |
+| `s_module_workflow` | `b_engine_key`、`b_options` |
-迁移步骤:
+报表、仪表盘和工作流只定义运行时契约,本阶段不提供报表引擎、设计器或流程引擎。
-1. 新建 list、edit、group、query 表和必要索引,新增 `s_module.b_module_type` 与 `s_module_field.b_type` 的兼容列。
-2. 为原有字段创建列表配置;根据原 `b_canform`、`b_canquery` 初始化编辑和查询配置。
-3. 前端和后端改为读取新场景表,查询不再读取 list 配置;模块类型切换为读取 `b_module_type`。
-4. 完成视图同步服务并验证新结构的读写稳定后,删除旧混合列和兼容字段。
+### `dbo.s_module_profile`
+
+Profile 只属于数据模块。同一模块内 `b_code` 唯一,并且只能有一个 `b_default = 1`。
+
+| 字段 | 说明 |
+| --- | --- |
+| `b_id` | 不可变雪花 ID |
+| `b_module_id` | 所属数据模块 |
+| `b_code` | 模块内唯一、不可变技术编码,如 `default`、`selector` |
+| `b_name` / `b_i18n` | 默认名称及 `meta.profile.` |
+| `b_default` / `b_canuse` / `b_xh` | 默认、启用和排序 |
+
+运行时列表通过 `module_id + profile_id` 定位;手写页面可以通过模块/Profile 技术编码解析稳定 ID。
+
+## 功能结构
+
+### `dbo.s_function_node`
+
+节点类型:
+
+- `directory`:只负责分组,不引用模块。
+- `entry`:引用一个 `page` Module。
+
+规则:
+
+- 技术编码全局唯一且不可修改。
+- 子节点只能放在 `directory` 下。
+- 入口节点不能包含子节点。
+- 数据库触发器和维护界面同时阻止循环移动。
+- 删除前必须清除角色和用户授权。
+
+菜单运行时读取 `v_s_function_node`,路由组件仍由前端白名单显式注册。
+
+## 页面编排
+
+### `dbo.s_page_node`
+
+节点类型:`group`、`tab`、`section`、`module_ref`。
+
+- 所有节点属于一个 `page` Module。
+- 容器节点不引用模块。
+- `module_ref` 可以引用 `data/report/dashboard/workflow` Module。
+- 引用数据模块时必须同时引用属于该模块的 Profile。
+- `module_ref` 不能包含子节点。
+- `b_permission_required = 1` 时需要页面节点授权。
+- `b_fixed_condition` 与 `b_param_mapping` 只保存运行时组合参数。
+
+页面运行时读取 `v_s_page_node`。`otherdata` 的分类和模块树迁入该表,不再保留 `b_other_bmfl`。
+
+## 能力模型
+
+### Module 级能力
+
+| 表 | 职责 |
+| --- | --- |
+| `s_module_field` | 数据字段基础定义,`(module_id, field)` 唯一 |
+| `s_module_action` | 稳定操作定义,`(module_id, code)` 唯一 |
+| `s_module_relation` | 主子模块字段关系 |
+| `s_module_auto_code` | 模块业务编号规则 |
+| `s_module_file` | 模块文件能力契约 |
+| `s_module_workflow_binding` | 模块事件/操作与工作流资源绑定 |
+
+### Profile 级能力
+
+| 表 | 职责 |
+| --- | --- |
+| `s_module_layout_group` | Profile 内列表/表单布局分组 |
+| `s_module_field_list` | 列表列、分组、可见性、顺序、宽度 |
+| `s_module_field_form` | 表单必填、只读、禁用、可见性和顺序 |
+| `s_module_field_query` | 查询控件、操作符、默认值和顺序 |
+| `s_module_import` | 导入方式与扩展参数 |
+| `s_module_export` | 导出方式与扩展参数 |
+
+字段同步按字段技术名合并:保留已有人工配置,新字段补默认列表/表单配置,数据源中失效的字段只停用、不删除。
+
+## 权限
+
+角色是主授权,用户授权只做加法:
+
+```text
+b_role
+ -> b_role_function_node
+ -> b_role_page_node
+ -> b_role_action
+ -> b_role_data_scope
+
+b_user
+ -> b_user_role
+ -> b_user_function_node
+ -> b_user_page_node
+ -> b_user_action
+```
+
+有效权限视图:
+
+- `v_s_user_function_permission`
+- `v_s_user_page_permission`
+- `v_s_user_action_permission`
+
+超级角色自动拥有全部启用权限。授权关系始终保存节点或操作 ID,不保存名称。当前由前端执行菜单、路由、页面节点和操作控制;后端数据范围和写操作拦截为后续扩展点。
+
+`b_role_data_scope` 预留 `all/self/department/custom` 数据范围契约及扩展参数。本阶段不在查询接口中执行该规则。
+
+## 多语言
+
+固定前端文案使用语义键,例如 `common.save`。动态元数据使用 ID 键:
+
+```text
+meta.module.
+meta.node.
+meta.profile.
+meta.group.
+meta.field.
+meta.action.
+meta.role.
+```
+
+动态实体保存 `b_name` 与 `b_i18n`,Vue 统一调用:
+
+```js
+appStore.t(record.b_i18n, record.b_name);
+```
+
+翻译缺失时回退 `b_name`,不会阻塞配置使用。
+
+## 运行时视图
+
+`034_module_runtime_views.sql` 建立:
+
+- `v_s_function_node`
+- `v_s_page_node`
+- `v_s_module_profile`
+- `v_s_module_runtime`
+- `v_s_module_field_list`
+- `v_s_module_field_form`
+- `v_s_module_field_query`
+- `v_s_module_action`
+- 三个用户有效权限视图
+
+视图只组合读取契约,不承载保存逻辑。所有多表配置仍通过一次 `/data/saveobjt` 在同一事务内提交。
+
+## 迁移与校验
+
+按顺序人工执行:
+
+1. `033_create_module_structure.sql`:一次性迁移有效旧元数据并删除旧模型。
+2. `034_module_runtime_views.sql`:建立运行时视图和领域约束触发器。
+3. `verify_module_architecture.sql`:只读检查子类型、Profile、父子规则、循环、旧表和多语言键。
+
+执行前必须备份数据库。迁移脚本不由应用启动自动执行。
diff --git a/code/fms/模块管理实施计划.md b/code/fms/模块管理实施计划.md
index 606cca7b..16b7e36b 100644
--- a/code/fms/模块管理实施计划.md
+++ b/code/fms/模块管理实施计划.md
@@ -1,248 +1,42 @@
-# 模块管理实施计划
+# 模块架构实施说明
-## 目标
+本文件记录当前模块架构的实现边界。表结构以 `新数据库结构.md` 为准。
-使用 `fms-api/config/dbconfigs/g3hd.properties` 指向的新数据库建立模块管理表结构,并将 `ModuleManageDemo.vue` 改造成真实页面。
+## 已实现范围
-本阶段只实现《新数据库结构.md》中的六张表,不实现 `select` 的选项来源和模块关联。
+- Module 资源基表与 page/data/report/dashboard/workflow 子类型表。
+- FunctionNode 菜单和页面入口树。
+- PageNode 页面分组、Tab、区域和模块引用树。
+- 数据模块 Profile 复用,自动创建 `default`,支持复制使用方式。
+- Module 级字段、操作、关系、自动编码、文件和工作流绑定契约。
+- Profile 级列表、表单、查询、导入和导出契约。
+- 角色主授权、用户角色和用户附加授权。
+- 前端菜单、路由、页面节点和模块操作权限控制。
+- ID 型动态多语言键和名称回退。
+- 运行时组合视图、树约束触发器和只读校验脚本。
+- 模块管理按功能结构、页面编排、模块资源拆分。
-## 实施原则
+## 保持不变
-- 不新增模块管理专用后端接口。
-- 查询复用 `/data/loaddata` 和 `/data/page`。
-- 保存复用 `/data/saveobjt`,一次请求完成多表事务。
-- 雪花 ID 复用 `/data/nextid`。
-- 不使用 `/data/loaddatabysql` 拼接模块管理 SQL。
-- 只操作 `g3hd.properties` 指向的 `fms` 数据库,不修改旧数据库。
-- 真实页面沿用 Demo 的左树右工作区和 Antdv Next Tree、Form、Table 设计。
+- `/data/loaddata`、`/data/page`、`/data/saveobjt` 等通用接口及结构。
+- 通用表格、文件组件和业务表数据。
+- 手写 Vue 页面中的业务事件与业务流程。
+- 前端显式页面组件白名单。
-## 第一阶段:建立数据库
+## 明确不实现
-### 建表脚本
+- 旧模块元数据兼容层和双写。
+- 报表引擎、仪表盘设计器和工作流引擎。
+- 后端数据范围执行和写操作权限拦截;只保留 `b_role_data_scope` 契约。
+- 业务表或业务数据重构。
-新增:
+## 切换步骤
-```text
-fms-api/config/migrations/001_module_management.sql
-fms-api/config/migrations/002_module_order_condition.sql
-fms-api/config/migrations/003_module_surrogate_key.sql
-```
+1. 备份 `FMS` 数据库。
+2. 执行 `033_create_module_structure.sql`。
+3. 执行 `034_module_runtime_views.sql`。
+4. 执行 `verify_module_architecture.sql`。
+5. 部署同一次提交中的前端代码。
+6. 回归普通列表、可编辑列表、`otherdata`、用户授权和模块管理。
-按以下顺序建立表:
-
-1. `s_module`
-2. `s_module_field`
-3. `s_module_power`
-4. `b_user_module`
-5. `b_user_power`
-6. `b_i18n`
-
-脚本包含:
-
-- 主键、外键和唯一约束。
-- 模块树、字段列表和个人权限所需索引。
-- `XACT_ABORT ON` 和显式事务。
-- `IF NOT EXISTS`,保证脚本可以重复执行。
-- 不删除、不修改任何已有业务表。
-
-### 初始化数据
-
-初始化:
-
-| 模块 | 类型 | 路由 |
-| --- | --- | --- |
-| `m_system` | `directory` | - |
-| `m_module` | `page` | `/system/modules` |
-
-为 `m_module` 初始化按钮:
-
-```text
-create 新增模块
-update 修改模块
-delete 删除模块
-grant 用户授权
-```
-
-根据明确的管理员账号从 `b_user` 取得用户雪花 ID,写入该用户的 `b_user_module` 和四项 `b_user_power`。找不到管理员时终止初始化,不能默认给所有用户授权。
-
-### 执行和验证
-
-- 从 `g3hd.properties` 读取连接信息,密码不打印到日志。
-- 使用 PowerShell `.NET SqlClient` 执行脚本(本机旧版 `sqlcmd` 无法完成 TLS 握手)。
-- 执行前确认目标为 `118.89.70.199:1433 / fms`。
-- 连续执行两次,第二次不得重复建表或重复初始化。
-- 查询 SQL Server 系统表核对字段、约束、外键和索引。
-
-## 第二阶段:登录用户信息
-
-当前 JWT 使用登录账号,新权限表使用 `b_user.b_id`,需要调整:
-
-- 登录时同时保留用户雪花 ID、账号和姓名。
-- JWT 继续使用账号作为会话标识。
-- 登录响应返回 `id`、`account` 和 `name`。
-
-本阶段不修改通用查询和保存接口的请求格式,也不在后端执行模块权限校验。
-
-## 第三阶段:使用通用接口完成数据闭环
-
-### 数据加载
-
-页面通过现有接口加载:
-
-| 数据 | 接口 | 数据源 |
-| --- | --- | --- |
-| 模块树 | `/data/loaddata` | `s_module` |
-| 模块字段 | `/data/loaddata` | `s_module_field` |
-| 操作权限定义 | `/data/loaddata` | `s_module_power` |
-| 多语言 | `/data/loaddata` | `b_i18n` |
-| 删除依赖检查 | `/data/loaddata` | `b_user_module`、`b_user_power` |
-
-配置数据量较小,模块、字段和操作权限可以一次加载后在前端按 `b_module_id` 分组,减少来回请求。
-
-### 雪花 ID
-
-新增以下数据前调用 `/data/nextid`:
-
-- `s_module_field.b_id`
-- `s_module_power.b_id`
-- `b_i18n.b_id`
-- `s_module.b_id`
-
-`s_module.b_id` 使用不可变雪花 ID;可修改的小写模块编码保存在 `s_module.b_code`。
-
-### 模块保存
-
-前端维护服务器快照和编辑副本,计算 inserts、updates、deletes,然后一次调用 `/data/saveobjt` 保存:
-
-```text
-s_module
-s_module_field
-s_module_power
-b_i18n
-```
-
-通用保存请求示意:
-
-```json
-[
- {
- "table": "s_module",
- "key_field": "b_id",
- "updates": []
- },
- {
- "table": "s_module_field",
- "key_field": "b_id",
- "inserts": [],
- "updates": [],
- "deletes": []
- }
-]
-```
-
-前端保存前校验:
-
-- 模块代码使用小写字母、数字和下划线,且在模块间唯一。
-- 父节点存在且不会形成循环。
-- 目录、页面、数据模块字段组合合法。
-- 同一模块字段名和按钮编码不重复。
-- 表名、视图名和字段名只允许合法数据库标识符。
-
-数据库外键和唯一约束负责最后兜底,任一表失败时由现有 `DataSaveService` 回滚整个请求。
-
-### 模块删除
-
-- `m_system` 和 `m_module` 在前端禁止删除。
-- 存在下级模块时禁止删除。
-- 普通模块删除时,一次 `/data/saveobjt` 按依赖顺序删除:
-
-```text
-b_user_power
-b_user_module
-s_module_power
-s_module_field
-b_i18n
-s_module
-```
-
-## 第四阶段:将 Demo 改为真实页面
-
-### 页面结构
-
-路由由 `/demo/modules` 改为 `/system/modules`,删除“前端原型”标识和 mock 数据。
-
-建议拆分:
-
-```text
-views/module-management/ModuleManagementView.vue
-views/module-management/components/ModuleTreePanel.vue
-services/moduleManagement.js
-```
-
-`services/moduleManagement.js` 只组合现有 `loadDataApi`、`saveObjectApi` 和 `nextIdApi`,不增加新的后端地址。
-
-### 页面功能
-
-- 左侧模块树支持搜索、新增目录和新增模块。
-- 左侧模块树只保留单个新增菜单,不直接显示目录、页面、数据三个按钮。
-- 模块管理界面只区分目录和普通模块:目录使用独立表单,新增目录使用 Dialog。
-- 页面模块和数据模块共用普通模块编辑器;配置页面路由时保存为 `page`,否则保存为 `data`,不直接编辑模块类型。
-- 基本信息、字段、操作权限和多语言全部读取真实数据库。
-- 字段、操作权限和多语言内容区独立滚动,页面外层不随数据量滚动。
-- 字段和操作权限采用点击行进入编辑、离开行退出编辑的单行编辑方式。
-- 字段名称支持在字段行内快速维护多语言文本。
-- 增加保存、取消和删除操作,当前阶段不显示未保存提示。
-- 保存失败时保留编辑内容并显示后端错误。
-- 字段编码、操作权限编码和模块代码使用明确的表单校验。
-- 删除字段、操作权限和模块使用确认框。
-- 修复 Demo 菜单预览使用不存在 `id` 作为 Vue key 的问题。
-- 保存成功后重新加载模块树和当前模块。
-
-### 菜单
-
-`AppSidebar` 通过现有通用查询接口读取当前用户的 `b_user_module` 和可用的 `s_module`,在前端构建菜单树。
-
-- 只显示 `b_canmenu = 1`、`b_canuse = 1` 且用户有权限的页面。
-- 上级目录根据可访问下级页面自动显示。
-- 路由组件仍由前端白名单注册,数据库路由不能动态加载任意组件文件。
-- 通用接口加载失败时保留现有静态菜单作为降级。
-
-### 前端权限
-
-- 数据型路由通过 `meta.dataCode` 声明核心数据编码,权限仓库根据数据节点的 `b_parent_id` 解析页面权限;尚未配置数据节点的系统页面暂时兼容 `meta.moduleCode`。
-- 用户没有 `b_user_module` 记录时禁止进入对应页面。
-- 页面操作根据 `b_user_power` 中的 `create`、`update`、`delete` 权限禁用。
-- 本阶段不改通用后端接口,也不做后端权限拦截;后端校验后续单独实施。
-
-## 第五阶段:测试与验收
-
-### 后端
-
-- JWT 能正确取得用户雪花 ID。
-- 多表保存失败时整体回滚。
-- 非法表名和字段名被现有通用接口拒绝,权限引用由数据库外键约束兜底。
-- `bigint` 仍以字符串返回前端。
-
-### 前端
-
-- 模块、字段、操作权限和多语言可以新增、修改、删除并刷新恢复。
-- 模块树搜索、切换和取消操作正常。
-- 加载失败、保存失败和空状态显示正确。
-
-### G3HD 验收
-
-1. 使用管理员账号登录 G3HD。
-2. 进入 `/system/modules`。
-3. 新建临时模块,配置两个字段和新增、修改、删除操作权限。
-4. 保存并刷新,确认所有配置重新加载。
-5. 删除临时模块,确认相关明细和已有个人权限全部清理。
-
-## 完成标准
-
-- 六张表已建立在 `g3hd.properties` 指向的新数据库。
-- 页面不再依赖 `moduleDemo.js`。
-- 没有新增模块管理专用后端接口。
-- 所有查询、保存、取号均复用现有通用接口。
-- 模块、字段、操作权限和多语言可以真实保存并重新加载。
-- 模块管理页面不提供用户授权功能。
-- 前端权限控制正常。
+迁移和服务启动均由执行人员明确操作,应用不会自动执行数据库迁移。
diff --git a/code/新建文件夹/cw/ys/detailbill.vue b/code/新建文件夹/cw/ys/detailbill.vue
index 76a009ed..38f02739 100644
--- a/code/新建文件夹/cw/ys/detailbill.vue
+++ b/code/新建文件夹/cw/ys/detailbill.vue
@@ -362,7 +362,7 @@ function buildSaveData() {
async function afterSave() {
await getMainTable(currentModule.value?.b_viewtable, maintable, maintableorg, maintable.value.b_id);
- await getSubTable("v_cw_ysbillsub", subtable, subtableorg, `father = '${maintable.value.b_id}'`);
+ await getSubTable(currentModule.value?.b_subtable, subtable, subtableorg, `father = '${maintable.value.b_id}'`);
op.value = "mod";
}
@@ -644,13 +644,13 @@ function calcAmountTotal() {
async function loadMod() {
await getMainTable(currentModule.value?.b_viewtable, maintable, maintableorg, id.value);
- await getColumnconfig("v_cw_ysbillsub", columns);
- await getSubTable("v_cw_ysbillsub", subtable, subtableorg, `father = '${id.value}'`);
+ await getColumnconfig(currentModule.value?.b_subtable, columns);
+ await getSubTable(currentModule.value?.b_subtable, subtable, subtableorg, `father = '${id.value}'`);
}
async function loadNew() {
await getDefaultValue(maintable, currentModule.value?.b_viewtable);
- await getColumnconfig("v_cw_ysbillsub", columns);
+ await getColumnconfig(currentModule.value?.b_subtable, columns);
maintable.value.b_id = await getSFId();
maintable.value.b_state = "草拟";
maintable.value.b_module_id = "应收";
@@ -664,7 +664,7 @@ async function handleToInvoiceByBill() {
}
// 根据账单查询费用明细
- const subdata = await loaddataApi("v_cw_ysbillsub", `father = '${maintable.value.b_id}'`);
+ const subdata = await loaddataApi(currentModule.value?.b_subtable, `father = '${maintable.value.b_id}'`);
if (!subdata.length) {
ElMessage.warning("未找到对应费用明细");
@@ -806,7 +806,7 @@ onMounted(async () => {
栏位设定