20260924172548

This commit is contained in:
oneao committed 2026-09-24 17:25:48 +08:00
1 parent 0405cd198a
commit 27e49e2e45
22 files changed
+1786 -346

No files matched your search

+34 -154
View File
@@ -1,167 +1,47 @@
# FMS 项目长期约定
# FMS 项目长期约定(主文件)
## 目录 / 文档 / 库
> 明细已拆分到 `memory/topics/`,需要时再读,避免主文件被截断:
> `topics/workflow.md`(工作流设置器 + dagre 布局)、`topics/frontend-ui.md`(主题/UI 库/i18n/导出)、
> `topics/env-and-baseline.md`(本机环境坑 + 测试基线)。每日日志见同目录 `YYYY-MM-DD.md`。
- 后端 `fms-api`(Spring Boot + JDK21,JDBC+Druid 无 ORM);前端 `fms-vue`(Vue 3.5 + Vite 8 + Pinia,纯 JS)。
`fms-vue-old1/old2` 仅供历史参考,**不要改**。
- 根目录设计文档:`FMS新系统核心表结构设计.md`(权威表结构)、`FMS业务表设计.md`、`开发规范.md`(AI 规范)、
`Fms旧系统表结构.md`(旧库 G3HY2025,只有表无视图)、`FMS删除规则引擎设计.md`。
- 建库/模块注册脚本在 `sql/`(`fms_core.sql`、`fms_business_*.sql`、`fms_module_*.sql`)。
- SQL Server `118.89.70.199:1433`:旧库 `G3HY2025`,新库 **FMS**(新系统一律用 FMS,**生产库,写前先确认**)。
- 机构→库映射在 `fms-api/config/dbconfigs/{ORG_ID}.properties`(gitignore):`G3HD.properties` → FMS,
`G3HY2025.properties` → G3HY2025。**没有 FMS.properties**。
- 后端 8088(context-path `/api`),前端 dev 5082(`/api` → 127.0.0.1:8088)。直连接口需先 `/auth/login`。
## 目录 / 库 / 端口
## 元数据核心表(真实列名,已连库核对,别再猜)
- 后端 `fms-api`(Spring Boot 4.1 + JDK21,JDBC+Druid 无 ORM);前端 `fms-vue`(Vue3.5 + Vite8 + Pinia,纯 JS,pnpm)。
`fms-vue-old1/old2` 仅历史参考,**不要改**。
- 根目录文档:`FMS新系统核心表结构设计.md`(权威表结构)、`FMS业务表设计.md`、`开发规范.md`(AI 规范)、
`Fms旧系统表结构.md`(旧库,只有表无视图)、`FMS删除规则引擎设计.md`;建库脚本在 `sql/`。
- SQL Server `118.89.70.199:1433`:旧库 `G3HY2025`,新库 **FMS**(生产库,写前先确认)。
机构→库映射 `fms-api/config/dbconfigs/{ORG_ID}.properties`(gitignore):`G3HD`→FMS、`G3HY2025`→旧库;
**没有 FMS.properties**。
- 后端 8088(context-path `/api`),前端 dev 5082(`/api`→127.0.0.1:8088)。直连接口需先 `/auth/login`。
- `s_module`:PK **`b_id`**(无 `b_module_code`;无 `b_route`,那是 `s_menu` 的);本身是树:
`b_parent_id`/`b_depth`/`b_path`;另有 `b_module_type`(module/data/virtual)、`b_view_table`、`b_save_table`、
`b_key_field`、`b_order_sql`、`b_query_sql`、`b_config_json`、`b_canuse`、`b_xh`、`b_bz`、`b_name`、`b_i18n`。
- `s_field`:PK **`(b_module_id, b_field)`**,无 `b_id`;`b_type` 实测 `input`/`number`/`checkbox`/`datetime`/`date`。
- `s_module_schema`:PK `(b_module_id, b_schema_type∈view|edit|query)`,`b_schema_json`。
- `s_menu`:`b_id`/`b_parent_id`/`b_depth`/`b_path`/`b_menu_type`(directory|page)/`b_route`/`b_icon`;`s_menu_module` 绑菜单与模块。
- **权限靠 `b_id`**:permissionStore 的 `moduleCodes` 即 `s_module.b_id`,页面 `MODULE_CODE` 必须等于它;
还要在 `s_menu_module` 里绑菜单,否则非超管看不到授权入口。
- 列表模块编码统一 `v_`+表名(`v_b_othercompany`),`b_view_table`→视图,`b_save_table`→真实表。
- schema JSON 形状:`view{columns:[{field,width}|{type:group,title,children}]}`、
## 元数据表真实列名(已连库核对,别猜)
- `s_module`:PK **`b_id`**(无 `b_module_code`;`b_route` 属 `s_menu`);树用 `b_parent_id`/`b_depth`/`b_path`;
另有 `b_module_type`(module/data/virtual)、`b_view_table`、`b_save_table`、`b_key_field`、`b_order_sql`、
`b_query_sql`、`b_config_json`、`b_canuse`、`b_xh`、`b_bz`、`b_name`、`b_i18n`。查询来源 `b_query_sql` > `b_view_table`。
- `s_field`:PK `(b_module_id, b_field)`,无 `b_id`;`b_type` 实测 input/number/checkbox/datetime/date。
- `s_module_schema`:PK `(b_module_id, b_schema_type∈view|edit|query)`,内容在 `b_schema_json`。
- `s_menu`:`b_id`/`b_parent_id`/`b_depth`/`b_path`/`b_menu_type`(directory|page)/`b_route`/`b_icon`;
`s_menu_module` 绑菜单与模块。
- **权限靠 `b_id`**:permissionStore.moduleCodes 即 `s_module.b_id`,页面 `MODULE_CODE` 必须等于它;
还需在 `s_menu_module` 绑菜单,否则非超管看不到授权入口。列表模块编码统一 `v_`+表名(`v_b_othercompany`)。
- schema JSON:`view{columns:[{field,width}|{type:group,title,children}]}`、
`edit{children:[{field,span,required,readonly,defaultValue}|group]}`、`query{groups:[{conditions}],quick}`。
- 路由守卫只看 `meta.moduleId`/`meta.adminOnly`;`views/module/**` 是 adminOnly(仅 g3soft)。
- 路由守卫只看 `meta.moduleId`/`meta.adminOnly`;`views/module/**` 是 adminOnly(仅 g3soft),不进 `s_menu`。
## 树结构规范(模块/菜单/部门/分类树通用)
## 树结构规范(模块/菜单/部门/分类通用)
`b_parent_id` 是权威关系,`b_depth`(根=0)、`b_path`(`/cn/east/`)是派生字段,移动节点要同事务更新全部后代;
不一致时以 `b_parent_id` 为准。工具在 `src/utils/tree.js`(`buildSortedTree`/`computeTreeMove`/`collectDescendantIds`/
`applyTreePosition`/`filterTreeByText`/`findTreeNode`),展示层用 `components/fms-tree/FmsTree.vue`(自管虚拟根)。
`b_parent_id` 权威,`b_depth`(根=0)/`b_path`(`/cn/east/`) 是派生字段,移动节点同事务更新全部后代;
不一致以 `b_parent_id` 为准。工具 `src/utils/tree.js`(buildSortedTree/computeTreeMove/collectDescendantIds/
applyTreePosition/filterTreeByText/findTreeNode),展示 `components/fms-tree/FmsTree.vue`(自管虚拟根)。
## 后端通用接口(前端业务数据只走这几个)
## 后端通用接口(业务数据只走这几个)
- `POST /auth/login`:字段是**小写** `orgid`/`userid`/`password`;`orgid=G3HD`。
- `/data/loaddata`:`view_name`/`search_condition`/**可选** `order_by`/`search_columns`(真 JSON 数组)。
- `/data/page`:**`order_by` 必填**(`DbUtils.buildPageOrderBy` 会报「order_by 不能为空」),
分页参数名是 **`page_no`/`page_size`**(不是 pageindex/pagesize)。
- `POST /auth/login`:字段**小写** `orgid`/`userid`/`password`;`orgid=G3HD`。
- `/data/loaddata`:`view_name`/`search_condition`/可选 `order_by`/`search_columns`(真 JSON 数组)。
- `/data/page`:**`order_by` 必填**;分页参数名 **`page_no`/`page_size`**。
- `/data/saveobjt`:body 是数组,每项 `{table, key_field, deletes, updates, inserts}`,**`key_field` 必填**
(复合主键逗号分隔),顺序 delete→update→insert。
- 模块元数据不由接口下发,前端读本地 schema JSON,改元数据无需重启后端。
- `DbUtils` 是 SQL 唯一出口:标识符加 `[]`,`validateReadOnlySql()` 只允许查询。
## 工作流流程设置器(fms-vue/src/views/system/workflow)
- **代码现状 = 原始版本(2026-09-23 曾被重构,应用户要求已全部回滚)**:
草稿是 `schemaVersion: 1`(`{definitionId, name, nodes, edges}`),编辑器是
`WorkflowDesigner` + `WorkflowCanvas`(LogicFlow) + `WorkflowPropertyPanel` 三件套,
节点坐标挂在 `node.x/y`,条件存单个叶子 `{field, operator, value}`,
写入通道未实现(工具栏只有撤销/缩放/查看草稿 JSON)。
**不要以为仓库里已经有卡片模式/流程树/条件构造器 —— 那些都被回滚了。**
回滚前的完整实现留在 `D:\workspace\code\fms-workflow-refactor-backup-20260923\`(仓库外,含 `http.js` 的 get/put、
`tests/unit/workflow-domain.spec.js`、改过 AST 形状的 mock SQL),需要时可取回。
- **文档是目标,不是现状**:`FMS审批流程设置器交互设计.md`(旧名 `FMS审批流程交互重构设计.md`,
已整体重写为 258 行)要求把设计器改成钉钉式递归流程树,并**明确作废**"线性卡片 + LogicFlow 高级模式"
双编辑器方案(§3.2 不再用画布兜底,并行/服务节点等拓扑只读保护);
`FMS审批流程设置器二次重构实施文档.md` 给出增量执行顺序(§1.1 的交互决策已锁定,不必再问);
`FMS工作流与审批设计.md` 管表结构与运行语义(其 §19 的画布选型是旧方案)。
参考交互项目:`D:\workspace\code\test\Workflow-Vue3`(只借交互,不复制代码/Element UI/私有字体)。
- **这些文件未进 git**:`design.vue`、`designer/`(除 `index.vue` 外全部)、`sql/fms_workflow_mock.sql`
都是未跟踪文件 —— 改它们之前先备份,`git restore` 救不回来(本地历史里也没有)。
- **`workflow/index.vue` 要小心**:它虽然被 git 跟踪,但**工作区版本比 HEAD 新**(新增按钮、表格「设计」行操作
都是未提交改动)。直接 `git restore` 会退回 HEAD 的 18 行空壳,把按钮冲掉 —— 要恢复就用带按钮的 76 行版本。
- 表结构已建好:`sql/fms_workflow.sql`(13 张表)、`fms_workflow_module.sql`(视图 + s_module 注册)、
`fms_workflow_menu.sql`(菜单 `workflow` / `approval*`)、`fms_workflow_mock.sql`(`expense_approval` 样本,
覆盖 7 种节点类型 / 6 种审批方式 / 5 种审批人解析)。
- **后端 `fms-api/src/main` 目前零工作流代码**:保存/校验/模拟/发布都要走流程服务,接口 404 时必须
明确提示"服务未接入",绝不伪造成功。
- 若将来重新做树形重构,踩过的坑:① `b_priority` 是**同一来源节点内**的判断顺序,不是全表序号;
② 校验只约束**出边**,入边不能锁成 1(分支汇合点天生多条入线);③ 环要单独拦,否则递归不终止;
④ 默认出口(`condition: null`)必须唯一且固定最后。
### 画布「一键整理」(dagre 自动布局,2026-09-23 已落地)
- 依赖 **`@logicflow/layout@2.1.5`**(peer 正好是 `@logicflow/core ^2.2.5`)。注意 dagre **不在**
`@logicflow/extension` 里(那只有一个标注"未完善"的 AutoLayout)。
- 必须**深路径导入** `import { Dagre } from '@logicflow/layout/es/dagre'`:包入口同时导出 `elkLayout`,
从入口引会把 elkjs 打进产物(实测走深路径只 +92 kB,走入口会 +1MB 以上)。包没有 `exports` 字段,深路径可用。
- 注册方式:`new LogicFlow({ plugins: [Dagre] })`,调用 `lf.extension.dagre.layout({...})`。
插件实例由 `installPlugin` 的"非静态插件"分支创建(`Dagre` 只有 `static pluginName` + `render`,没有 `install`)。
- **坑**:插件靠 `ToolOverlay` 的 `componentDidMount` → `triggerToolRender` 调用 `render(lf)` 才拿到
`this.lf`;拿不到时 `layout()` 会**直接抛错**(它的 try/catch 只包住 applyDagreLayout)。
所以 `autoLayout()` 里先兜底 `if (!dagre.lf) dagre.lf = lf`,并用 try/catch + 「坐标是否变化」判断结果
—— 插件失败时只打 console.error,不能假定成功。
- 布局走 `lf.renderRawData()` 整图重渲染:**会丢选中态**,但节点/连线 **id 保留**(`processEdges` 只改路径字段),
所以上层按 nodeKey/edgeKey 回填审批人与条件仍然成立。坐标变化由实例的 `history` 自动记录(deepObserve + debounce)。
- 布局后**必须 `snapshot()`** 把新坐标 emit 给上层,否则草稿里的 x/y 还是旧的。
- 参数:`rankdir: 'TB'`(审批流程纵向)、`align: 'UL'`、`nodesep: 60`、`ranksep: 80`、`isDefaultAnchor: true`(重算连线路径)。
- 测试:`tests/unit/workflow-canvas-layout.spec.js`(jsdom 挂载画布真跑一次布局;需给 `SVGElement.prototype.getBBox` 打桩,
否则 LogicFlow 的 autoWrap 文本测量会挂)。
## 环境坑(本机特有)
- **编译/测试 fms-api 唯一可行命令**(start.cmd 里的 JDK/Maven 路径都是坏的):
```
cd /d/workspace/code/fms/fms-api && /d/devtool/jdk/jdk21/bin/java \
-Dclassworlds.conf="D:/devtool/maven/bin/m2.conf" -Dmaven.home="D:/devtool/maven" \
-Dmaven.multiModuleProjectDirectory="D:/workspace/code/fms/fms-api" \
-cp "D:/devtool/maven/boot/plexus-classworlds-2.11.0.jar" \
org.codehaus.plexus.classworlds.launcher.Launcher -o -Dtest=XxxTests test
```
本地仓库是 `D:\data\maven`(不是 ~/.m2);联网下载去掉 `-o`;`target/` 被 git 跟踪,编译后有噪音。
- 无 sqlcmd/pyodbc/pymssql:用 `fms-api/tools/migration/RunSqlFile.java` 或一次性 `SqlRunner.java`
+ JDBC jar(`~/.m2/.../mssql-jdbc-13.4.0.jre11.jar`)+ `/d/devtool/jdk/jdk17/bin/java`。
脚本要按行首 `GO` 分批,路径传绝对路径。
- `vite build` 被 safe-delete 守卫拦(要删 600+ 文件):用 `--outDir dist-verify --emptyOutDir`;
清理用 `find dist-verify -type f -delete` 再 `find dist-verify -type d -empty -delete`。
- `oxfmt --check` 全仓库都报 format issues(版本漂移),**不是自己引入的**,别顺手格式化整个仓库。
- 本工作区可能被多会话同时编辑,验证失败项先用 `find <dir> -mmin -N` 确认是否自己引入。
## 测试基线(既有失败,别去修)
- 后端 `OrgDataSourceFactoryTests.databaseConnectionFailureDoesNotRetry`(断言 Druid retry=0,实测 1)。
- 前端 `tests/unit/fms-module-list-page.spec.js` 里引用 `.filter-drawer`/`.filter-row__*` 的用例
(类名已不存在,是高级查询面板改版后的过期断言)—— 另有约 13 个长期失败基线。
- `tests/helpers/app.js` 的 `mountPage` 用 shallowMount 且默认打桩子组件:给 `Splitter`/`SplitterPanel`
打桩时键名要写 **Vue 推断名 `'splitter'`/`'panel'`**(文件名小写),写 `'SplitterPanel'` 打不上桩;
自定义组件传 `false` 可强制真实挂载。`global` 会整体覆盖内部 stubs,别在里面再写 stubs。
## 主题约定
- 配色唯一真相 `fms-vue/src/theme/tokens.css`:`:root` 浅色 + `.dark` 深色必须同步增删同名变量,
`--fms-primary` 由 JS 写入,衍生色用 `color-mix()`。改主题只动这个文件。
- **侧栏当前是浅色的**(不存在 `--fms-sidebar-*` 变量;若再看到「侧栏是黑的」即为过期信息)。
要改侧栏配色就新增 `--fms-sidebar-*` 只给 `AppSidebar.vue`/`NavMenuItem.vue` 用,
**别动 `--fms-surface`/`--fms-text` 等全局中性色**(顶栏与所有业务页共用)。
- 深底+主题色高亮必须用「深底 + 白字 + 主色亮描边」:填充 `color-mix(primary 38%, #000)`、
描边 `color-mix(primary 40%, #fff)`;「浅底+白字」对比度只有 1.4~2.7:1,不可用。
- Teleport 到 body 的浮层(如 `.fms-team-menu-*`)不在侧栏内,保持全局浅色 token。
## UI 组件库全局配置
- 唯一入口 `fms-vue/src/components/ui/config.js`:`uiConfig`(reactive)+ `setUIConfig(patch)`(按组件维度浅合并)
+ `resetUIConfig()`,也从 `@/components/ui` 具名导出。**没有 ConfigProvider、没有 app.use 插件**。
- 回落顺序固定 **自身 prop > uiConfig > 组件内置默认**;要支持全局覆盖的 prop,`default` 必须写 `undefined`
且 validator 要放行 `undefined`(Vue 会拿 default 跑校验)。
- Form 的 `layout`/`labelAlign`/`labelWidth`/`validateTrigger` 已接入;provide 下发回落后的最终值。
语言联动在 `src/App.vue`:中文 `horizontal`、其他语言 `vertical`(判定 `/^zh/i`)。
- **多列紧凑网格的表单必须显式 `layout="vertical"`**(如自动编码面板 columns=5),
否则横排的 88px 标签会把控件压到几十像素。prop 优先于全局配置。
- `defineModel` 写完**不能立刻回读**:写了只 emit,prop 下一轮才回来;要取刚写入的值就自己先
`reactive()` 包一层再赋值,返回其中的代理对象。
## i18n 运行时
- 语言清单 `s_i18n_type`(`b_id` 即语种编码,zh-CN 默认 / en-US);译文 `s_i18n(b_key, b_locale, b_value)`。
- store `stores/i18n.js`:`loadLanguages()` 拉清单、**`loadMessages(locale)` 拉译文**(按语种缓存 + 并发去重 +
失败静默;只把「当前语种」的译文写进运行时)。**别只看 locale——不调 loadMessages 界面不会变。**
- 合并顺序 **前端内置 < 库表译文**:内置只有 en-US 的 `common.*`(`src/i18n/builtin-messages.js`,
代码约定 `t('common.x', '中文兜底')`);业务文案/字段名/菜单名的译文一律进 `s_i18n` 由实施维护。
- 现状:`s_i18n` 只登记了 zh-CN(约 134 条),en-US 尚无数据 → 英文界面只有框架按钮/提示是英文,
字段名等仍回落中文,需要补录。
- 顶栏语种下拉取自 `s_i18n_type` 清单,不要硬编码语言编码。
- 查询区(`FmsQueryToolbar`)已改用 Form/FormItem,因此也跟随该配置;Form 根靠 `display: contents`
透明化,让 FormItem 直接参与 `.query-grid`(`minmax(0,200px)`)布局,label 宽局部 72px。
高级查询面板(`FmsAdvancedQueryPanel`)仍是自绘 `.filter-*`,**不受**表单布局配置影响。
## 导出(P0 已落地,摘要)
`POST /api/export/list` 返回文件流:`SqlPermissionService` → `ExportSchemaService`(列与列表同源推导)
→ `ExportService`(Fesod 流式写)→ xlsx。**导出列不由前端传**,否则字段权限形同虚设;
校验必须在 `getOutputStream()` 前完成。依赖 `org.apache.fesod:fesod-sheet:2.0.2-incubating`(自带 POI,别再引)。
Fesod 坑:0 行也要调一次 `write`,否则无工作表;合计行要自己构造标签。
权限现状:`s_power` 只有 2 行,非超管会被拒,g3soft 可直接用。