From 38e1e66c3b4b7c628a2d1922cd14bdb0f8d05fb4 Mon Sep 17 00:00:00 2001 From: ZhangAo Date: Thu, 17 Sep 2026 16:58:40 +0800 Subject: [PATCH] 20260917165839 --- code/fms/.workbuddy-ai/memory/2026-09-17.md | 17 + code/fms/.workbuddy-ai/memory/MEMORY.md | 253 ++++-------- code/fms/FMS导出功能设计与进度.md | 353 ----------------- code/fms/FMS新系统核心表结构设计.md | 24 +- .../g3soft/fmsapi/config/JwtAuthFilter.java | 10 +- .../fmsapi/controller/ExportController.java | 9 +- .../fmsapi/service/ActiveSessionRegistry.java | 12 +- .../fmsapi/service/ExportSchemaService.java | 44 ++- .../g3soft/fmsapi/service/ExportService.java | 49 ++- .../fmsapi/service/ExportWorkbookWriter.java | 3 +- .../fmsapi/config/JwtAuthFilterTests.java | 124 ++++++ .../service/ActiveSessionRegistryTests.java | 49 ++- .../ExportServiceIntegrationTests.java | 80 +++- .../fmsapi/service/ExportServiceTests.java | 30 +- .../g3soft/fmsapi/config/JwtAuthFilter.class | Bin 4601 -> 4486 bytes .../fmsapi/controller/ExportController.class | Bin 5477 -> 6296 bytes .../ActiveSessionRegistry$AccountKey.class | Bin 1790 -> 1790 bytes .../service/ActiveSessionRegistry.class | Bin 1793 -> 1625 bytes .../fmsapi/service/ExportSchemaService.class | Bin 13694 -> 15839 bytes .../fmsapi/service/ExportService$1.class | Bin 1130 -> 1117 bytes .../service/ExportService$ExportRequest.class | Bin 3139 -> 3542 bytes .../ExportService$PreparedExport.class | Bin 2782 -> 3076 bytes .../ExportService$ResultSetRowSource.class | Bin 3140 -> 3249 bytes .../g3soft/fmsapi/service/ExportService.class | Bin 28738 -> 28716 bytes .../fmsapi/service/ExportWorkbookWriter.class | Bin 5174 -> 5291 bytes .../default-testCompile/createdFiles.lst | 1 + .../default-testCompile/inputFiles.lst | 1 + ...3soft.fmsapi.config.JwtAuthFilterTests.xml | 73 ++++ ...api.service.ActiveSessionRegistryTests.xml | 51 ++- ...g3soft.fmsapi.service.AuthServiceTests.xml | 46 +-- ....service.ExportServiceIntegrationTests.xml | 80 ++-- ...soft.fmsapi.service.ExportServiceTests.xml | 77 ++-- ...sapi.service.ExportWorkbookWriterTests.xml | 52 +-- ...T-cn.g3soft.fmsapi.utils.JwtUtilsTests.xml | 46 +-- ...3soft.fmsapi.config.JwtAuthFilterTests.txt | 4 + ...api.service.ActiveSessionRegistryTests.txt | 2 +- ...g3soft.fmsapi.service.AuthServiceTests.txt | 2 +- ....service.ExportServiceIntegrationTests.txt | 2 +- ...soft.fmsapi.service.ExportServiceTests.txt | 2 +- ...sapi.service.ExportWorkbookWriterTests.txt | 2 +- .../cn.g3soft.fmsapi.utils.JwtUtilsTests.txt | 2 +- .../fmsapi/config/JwtAuthFilterTests.class | Bin 0 -> 8112 bytes .../service/ActiveSessionRegistryTests.class | Bin 1598 -> 4780 bytes .../ExportServiceIntegrationTests.class | Bin 13872 -> 18831 bytes .../fmsapi/service/ExportServiceTests.class | Bin 13167 -> 15663 bytes .../fms-vue/src/components/I18nNameModal.vue | 2 +- .../src/components/captcha/SliderCaptcha.vue | 2 +- .../src/components/fms-file/FileCard.vue | 4 +- .../src/components/fms-file/FileListPanel.vue | 6 +- .../components/fms-file/FilePreviewPanel.vue | 6 +- .../src/components/fms-file/FmsFile.vue | 3 +- .../fms-module-common/schemaRender.js | 16 +- .../fms-module-edit/FmsModuleEditModal.vue | 22 +- .../fms-module-list/FmsAdvancedQueryPanel.vue | 4 +- .../fms-module-list/FmsExportDialog.vue | 85 +++- .../fms-module-list/FmsModuleListPage.vue | 43 +- .../fms-module-list/FmsModuleTable.vue | 21 +- .../fms-module-list/FmsQueryControl.vue | 11 +- .../fms-module-list/FmsQueryToolbar.vue | 2 +- .../src/components/fms-select/FmsSelect.vue | 13 +- .../src/components/fms-table/FmsTable.vue | 8 +- .../src/components/fms-tree/FmsTree.vue | 26 +- .../src/components/ui/date/calendar-panel.vue | 13 +- .../src/components/ui/date/date-picker.vue | 15 +- .../fms-vue/src/components/ui/date/index.scss | 95 ++--- .../src/components/ui/date/range-picker.vue | 18 +- .../src/components/ui/input/index.scss | 48 +-- .../src/components/ui/notification/index.scss | 2 +- .../src/components/ui/select/index.scss | 43 +- .../fms-vue/src/components/ui/tree/index.scss | 2 +- .../components/ui/utils/_control-shell.scss | 66 ++++ .../src/layouts/components/AppTopbar.vue | 2 +- .../fms/fms-vue/src/services/exportService.js | 10 +- code/fms/fms-vue/src/theme/tokens.css | 6 +- code/fms/fms-vue/src/utils/schemaJson.js | 122 +++++- .../src/views/base/othercompany/index.vue | 2 + .../src/views/base/otherdata/index.vue | 2 +- .../fms/fms-vue/src/views/dashboard/index.vue | 12 +- .../fms-vue/src/views/detail/DetailPage.vue | 6 +- .../fms/fms-vue/src/views/file-demo/index.vue | 2 +- code/fms/fms-vue/src/views/login/index.vue | 6 +- .../views/module/menu-management/index.vue | 1 - .../module-management/ModuleEditPanel.vue | 3 + .../ModuleListConfigPanel.vue | 3 + .../module-management/ModulePowerPanel.vue | 86 ++-- .../ModuleQueryConfigPanel.vue | 11 +- .../module-management/ModuleTreePanel.vue | 15 + .../design-editor/FormDesignEditor.vue | 35 +- .../design-editor/TableDesignEditor.vue | 6 +- .../design-editor/common/FieldLibrary.vue | 5 +- .../design-editor/common/schemaModel.js | 31 +- .../common/useStructureEditing.js | 38 +- .../design-editor/form/FormInspector.vue | 4 +- .../design-editor/form/FormPreview.vue | 2 +- .../design-editor/table/TableInspector.vue | 8 +- .../views/module/module-management/index.vue | 173 ++++++-- .../views/module/module-management/utils.js | 153 ++++++-- .../tests/components/fms-tree-panel.spec.js | 61 +++ .../tests/services/export-service.spec.js | 33 ++ .../tests/unit/fms-export-dialog.spec.js | 42 +- .../tests/unit/fms-module-list-page.spec.js | 89 ++++- .../tests/unit/module-i18n-reconcile.spec.js | 43 +- .../unit/module-query-config-panel.spec.js | 138 +++++++ .../unit/module-table-edit-panel-dnd.spec.js | 5 + .../fms-vue/tests/unit/schema-json.spec.js | 91 +++++ .../views/module-unsaved-after-save.spec.js | 370 ++++++++++++++++++ .../fms-vue/tests/views/othercompany.spec.js | 18 + code/fms/开发规范.md | 2 +- 108 files changed, 2497 insertions(+), 1215 deletions(-) create mode 100644 code/fms/.workbuddy-ai/memory/2026-09-17.md delete mode 100644 code/fms/FMS导出功能设计与进度.md create mode 100644 code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/config/JwtAuthFilterTests.java create mode 100644 code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.config.JwtAuthFilterTests.xml create mode 100644 code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.config.JwtAuthFilterTests.txt create mode 100644 code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/config/JwtAuthFilterTests.class create mode 100644 code/fms/fms-vue/src/components/ui/utils/_control-shell.scss create mode 100644 code/fms/fms-vue/tests/components/fms-tree-panel.spec.js create mode 100644 code/fms/fms-vue/tests/services/export-service.spec.js create mode 100644 code/fms/fms-vue/tests/unit/module-query-config-panel.spec.js create mode 100644 code/fms/fms-vue/tests/unit/schema-json.spec.js create mode 100644 code/fms/fms-vue/tests/views/module-unsaved-after-save.spec.js diff --git a/code/fms/.workbuddy-ai/memory/2026-09-17.md b/code/fms/.workbuddy-ai/memory/2026-09-17.md new file mode 100644 index 00000000..3a4a4d08 --- /dev/null +++ b/code/fms/.workbuddy-ai/memory/2026-09-17.md @@ -0,0 +1,17 @@ +# 2026-09-17 + +## 修复:合作伙伴分类删除报「order_by 不能为空」 + +- 现象:`base/othercompany` 删除分类 → 「分类数据检查失败:order_by 不能为空」。 +- 根因:`onCategoryDelete` 的守卫查询调 `pageDataApi`(`/data/page`)**漏传 `orderBy`**; + 后端 `DataService.page` 用 `getRequiredString(params,"order_by")`,`DbUtils.buildPageOrderBy` + 对空值抛「order_by 不能为空」。`/data/loaddata` 的 order_by 是可选的,`/data/page` 是必填 + (分页需要确定性排序),所以同类守卫在 `views/system/user` 用 loaddata 没问题、这里用 page 就炸。 +- 改动:`src/views/base/othercompany/index.vue` 守卫查询加 `orderBy: 'b_id ASC'`; + `tests/views/othercompany.spec.js` 新增回归用例断言 pageDataApi 的入参(含 orderBy)。 + 全套 10 个用例通过;oxlint 干净(oxfmt 全仓库既有 format issues,未动)。 +- 已核对仓库内另 3 处 `pageDataApi` 调用(FileListPanel / useLookupOptions / FmsModuleListPage)都传了 orderBy。 + +## 维护 + +- `MEMORY.md` 超限被截断,已按主题重写精简(合并重复项、删掉过期与可推导内容)。 diff --git a/code/fms/.workbuddy-ai/memory/MEMORY.md b/code/fms/.workbuddy-ai/memory/MEMORY.md index a18a4f6a..fe35a944 100644 --- a/code/fms/.workbuddy-ai/memory/MEMORY.md +++ b/code/fms/.workbuddy-ai/memory/MEMORY.md @@ -1,195 +1,92 @@ # FMS 项目长期约定 -## 目录与文件 +## 目录 / 文档 / 库 -- 后端 `fms-api`,前端 `fms-vue`(Vue 3.5 + Vite 8 + Pinia 4 + pnpm,**纯 JS 无 TS**)。 -- 设计文档集中在仓库根:`FMS新系统核心表结构设计.md`(新库 FMS)、`FMS业务表设计.md`、 - `Fms旧系统表结构.md`(旧库 G3HY2025,**只有表、不含视图**)、`开发规范.md`、`FMS删除规则引擎设计.md`。 -- 建库/注册脚本在 `sql/`:`fms_core.sql`、`fms_business_*.sql`(建表+视图)、`fms_module_*.sql`(模块元数据)。 +- 后端 `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`。 -## 数据库 +## 元数据核心表(真实列名,已连库核对,别再猜) -- 服务器 `118.89.70.199:1433`;**旧库 `G3HY2025`**、**新库 `FMS`**(新系统一律用 FMS)。 -- 机构与库的映射写在 `fms-api/config/dbconfigs/{ORG_ID}.properties`(该目录被 gitignore)。 - 已存在:`G3HD.properties` → databaseName=**FMS**;`G3HY2025.properties` → databaseName=G3HY2025。 - **没有 FMS.properties**,G3HD 就是指向新库的那份。 -- 后端 `fms-api` 监听 **8088**(context-path `/api`);前端 dev server **5082**(proxy `/api` → 127.0.0.1:8088)。 -- 直接 curl 后端接口会返回 `401 未登录或登录已失效`,需先 `/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}]}`、 + `edit{children:[{field,span,required,readonly,defaultValue}|group]}`、`query{groups:[{conditions}],quick}`。 +- 路由守卫只看 `meta.moduleId`/`meta.adminOnly`;`views/module/**` 是 adminOnly(仅 g3soft)。 -## 元数据驱动的核心约定 +## 树结构规范(模块/菜单/部门/分类树通用) -- 模块树 `s_module`(`b_module_type` ∈ module/data/virtual),字段 `s_field`, - 界面配置 `s_module_schema`(PK = `b_module_id` + `b_schema_type` ∈ view/edit/query,JSON 存在 `b_schema_json`)。 -- **这三张表的真实列名(已连库核对,写脚本时别再猜)**: - - `s_module`:PK 是 **`b_id`**(**没有** `b_module_code` 列);且它**本身就是树** —— - `b_parent_id` / `b_depth` / `b_path`;另有 `b_module_type`(`module` / `data`)、 - `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_route`**(那是 `s_menu` 的列)。 - - `s_field`:PK 是 **`(b_module_id, b_field)`**,**没有 `b_id`**;列为 - `b_field` / `b_name` / `b_i18n` / `b_type` / `b_canuse` / `b_xh`。`b_type` 实测取值: - `input` / `number` / `checkbox` / `datetime` / `date`。 - - `s_module_schema`:PK `(b_module_id, b_schema_type)`,另有 `b_schema_json` / `b_canuse`。 - - `s_menu`:`b_id` / `b_parent_id` / `b_depth` / `b_path` / `b_name` / `b_i18n` / - `b_menu_type`(`directory` 或 `page`)/ `b_route` / `b_icon` / `b_xh` / `b_canuse`。 - - `s_menu_module`:`b_menu_id` / `b_module_id` / `b_xh` / `b_canuse`。 -- **模块编码写法**:列表模块统一 `v_` + 表名(如 `v_b_contact`),`b_view_table` 指向查询视图, - `b_save_table` 指向真实表。数据模块挂在分组 module 节点下(如 `base`、`system`)。 -- **前端权限靠 `b_id`**:`usePermissionStore` 的 `moduleCodes` 就是 `s_module.b_id`; - `canPower(code, power)` 查 `${b_id}.${power}`。所以页面里写的 `MODULE_CODE` 必须等于模块 `b_id`。 - 另外要记得在 `s_menu_module` 里把菜单绑到模块,否则侧栏入口不会出现在角色的「菜单权限」树中,非超管无法授权。 -- **s_module_schema JSON 真实形状**(与 `schemaRender.js` 一一对应,别写错): - - `view`:`{ schemaVersion, columns: [ {field,width} | {type:'group',title,children} ] }` - - `edit`:`{ schemaVersion, children: [ {field,span,required,readonly,defaultValue} | {type:'group',...} ] }` - - `query`:`{ schemaVersion, groups: [ { conditions: [ {field,operator} ] } ], quick: [field] }` -- **s_menu 无种子数据**,业务菜单由「菜单管理」页面维护。`s_menu_module` 把菜单关联到 data/virtual 模块。 -- 路由守卫只看 `meta.moduleId` / `meta.adminOnly`(`to.matched.some`),**没有 moduleId 的路由不需要模块记录即可访问**。 +`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`(自管虚拟根)。 -## 树结构规范(模块树 / 菜单树 / 部门树通用) +## 后端通用接口(前端业务数据只走这几个) -见 `FMS新系统核心表结构设计.md` 第 1.7 节:**邻接表为主 + `b_depth`/`b_path` 冗余**。 +- `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)。 +- `/data/saveobjt`:body 是数组,每项 `{table, key_field, deletes, updates, inserts}`,**`key_field` 必填** + (复合主键逗号分隔),顺序 delete→update→insert。 +- 模块元数据不由接口下发,前端读本地 schema JSON,改元数据无需重启后端。 +- `DbUtils` 是 SQL 唯一出口:标识符加 `[]`,`validateReadOnlySql()` 只允许查询。 -- `b_parent_id` 是**权威关系**;`b_depth`、`b_path` 是业务层维护的**派生字段**。 -- 根节点 `b_parent_id` 为 `NULL`,`b_depth` 从 0 起算;`b_path` 形如 `/cn/east/`(前后都带 `/`)。 -- 新增/移动节点时必须在同一事务中同步当前节点及**全部后代**的 `b_depth`/`b_path`。 -- 二者不一致时**以 `b_parent_id` 为准**,并提供按父子关系重建冗余字段的能力。 -- 全项目通用的树工具在 **`fms-vue/src/utils/tree.js`**(`buildSortedTree` / `computeTreeMove` / - `collectDescendantIds` / `applyTreePosition` / `filterTreeByText` 等)。新增一棵树优先复用它。 -- 展示层适配器 **`fms-vue/src/components/fms-tree/FmsTree.vue`**:自管 `loading` 遮罩与 - 「全部分类」虚拟根(空选中键 ⟺ 虚拟根,调用方不需要知道 `rootKey`)。 +## 环境坑(本机特有) -## 主题与样式约定 +- **编译/测试 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 -mmin -N` 确认是否自己引入。 -- 全站配色唯一真相是 **`fms-vue/src/theme/tokens.css`**:浅色定义在 `:root`, - 深色覆盖在 `.dark`,**两组必须同步增删同名变量**;`--fms-primary` 由 JS 写入, - 衍生色一律用 `color-mix()` 派生。改主题只动这个文件,不在组件里硬编码颜色。 -- **侧栏当前是浅色的**(`--fms-surface` 白底 + `box-shadow: var(--fms-shadow-right)`), - 与顶栏同为「框架面」。2026-09-16 曾改造为黑色并已**按用户要求全部回退**, - 现存代码里**没有** `--fms-sidebar-*` 变量。若再看到「侧栏是黑的」的描述即为过期信息。 -- 主色预设(`src/theme/presets.js`)**默认是「墨蓝」`#0f172b`,本身就是近黑**。 - 所以任何深色底上做主题色元素时,别直接套浅底模板(见下条实测结论)。 -- **深色底 + 主题色高亮态的实测结论(回退前验证过,重做深色侧栏时直接复用)**: - 「浅底 + 白字」走不通 —— 主色向白混到能看清色相时已成 pastel,白字对比度实测仅 - **1.4~2.7:1**(9 个预设色全不合格)。必须用「深底 + 白字 + 主色亮描边」三件套: - 填充 = `color-mix(primary 38%, #000)`(白字 5.1~19.2:1 达标), - 描边 = `color-mix(primary 40%, #fff)`(近黑主色填充与黑底仅 1.04:1,全靠描边勾边界)。 - 即**文字可读性靠填充、可辨识性靠描边,二者分离**才能覆盖全部预设色。 -- **要改侧栏配色时的正确做法**:新增一组 `--fms-sidebar-*` 专用 token, - 只给 `layouts/components/AppSidebar.vue` 与 `NavMenuItem.vue` 用。 - **不要改 `--fms-surface` / `--fms-text` 等全局中性色** —— 那些是顶栏、内容卡、 - 全部业务页面共用的,改了会让整个主区一起变。 -- **Teleport 到 `body` 的浮层不算侧栏内元素**(如团队下拉 `.fms-team-menu-*`): - 它们渲染在浅色浮层容器里,改侧栏配色时必须保持全局浅色 token,不能跟着一起改。 +## 测试基线(既有失败,别去修) -## 测试 +- 后端 `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。 -- 测试 harness 在 `fms-vue/tests/helpers/app.js`:`mountPage` 内部用 **shallowMount**,默认打桩所有子组件。 -- **陷阱**:给布局容器(`Splitter`/`SplitterPanel`)打桩时,键名要写 **Vue 推断出的组件名**。 - 这两个 SFC 文件名是 `splitter.vue` / `panel.vue`,推断名是 `'splitter'` / `'panel'`, - **不是** `'SplitterPanel'`;写错键就打不上桩,容器成空壳、插槽不渲染。 - 自定义组件(如 `DeptTreePanel`)传 `false` 可强制真实挂载。 -- `mountPage` 的 `global` 选项会整体覆盖内部 `stubs`,不要在 `global` 里再写 `stubs`。 -- 既有 13 个失败用例(query-UI / router-auth / login / scratch repro)是长期基线,非新改动引入。 +## 主题约定 -## 环境坑 +- 配色唯一真相 `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。 -- `vite build` 会被本机 safe-delete 守卫拦下(默认输出目录要删 672 个文件 > 阈值 50), - 验证构建请用 `--outDir dist-verify --emptyOutDir`,用完清理。 -- 清理 `dist-verify`(600+ 文件)时 `rm -rf` 同样被 safe-delete 拦(genie-trash 报 - `Some operations were aborted`)。可行做法:`find dist-verify -type f -delete`, - 再 `find dist-verify -type d -empty -delete`。 -- 本工作区**可能被多个会话同时编辑**,验证失败项前先用 `find -mmin -N` 按 mtime 确认是否自己引入。 +## 导出(P0 已落地,摘要) -## 直连 FMS 库执行 SQL(本机无 sqlcmd / pymssql / pyodbc 时) - -本机**没有** `sqlcmd`,Python 也**没有** `pymssql` / `pyodbc`。但有两样现成资源可拼出可用客户端: - -- JDBC 驱动:`C:/Users/admin/.m2/repository/com/microsoft/sqlserver/mssql-jdbc/13.4.0.jre11/mssql-jdbc-13.4.0.jre11.jar` -- JDK:`/d/devtool/jdk/jdk17/bin/java`(另有 jdk21 / jdk8 目录) - -做法:写一个一次性 `SqlRunner.java`,读脚本文件 → 按**独立成行的 `GO`** 分批 → 逐批 -`Statement.execute` 并把结果集打成对齐表格。编译运行: - -``` -/d/devtool/jdk/jdk17/bin/javac -d . SqlRunner.java -/d/devtool/jdk/jdk17/bin/java -cp ".;" SqlRunner "D:/abs/path/to.sql" -``` - -注意: -- **脚本没有 `GO` 就会被当成一个批次整体发送**,中途一句报错会导致后面全不执行; - 自检/探查脚本记得用 `GO` 分开。 -- `SqlRunner` 传相对路径会以 `cd` 后的目录解析,**一律传绝对路径**避免踩坑。 -- 连接串取自 `fms-api/config/dbconfigs/G3HD.properties`(gitignored),指向**远端生产库** - `118.89.70.199:1433` / `FMS`。**改动生产库前先探查现状、并清理自己造的测试数据。** -- 该库是**生产环境**,执行任何写操作前先确认清楚。 - -## 直连后端 API(比查库更接近真实行为) - -后端 `fms-api` 跑在 **8088**(context-path `/api`)。验证数据/权限链路时优先走它: - -- 登录 `POST /api/auth/login`:字段是**小写** `orgid` / `userid` / `password`。 - `orgid` 传 `G3HD`(对应 `dbconfigs/G3HD.properties` → `FMS` 库);缺了报「机构码不能为空」。 -- `POST /api/data/loaddata`:`view_name` / `search_condition` / `order_by` / `search_columns`(JSON 数组)。 -- `POST /api/data/page`:追加 `page_no` / `page_size`。 - **参数名是 `page_no`/`page_size`,不是 `pageindex`/`pagesize`**;`search_columns` 必须是**真 JSON 数组**, - 传字符串会报「search_columns 参数类型不正确」。 -- `POST /api/data/saveobjt`:body 是**数组**,每项形如 - `{table, key_field, deletes, updates, inserts}` —— **`key_field` 必填**,缺了报「key_field 不能为空」; - 复合主键用逗号分隔(如 `s_field` 传 `b_module_id,b_field`)。执行顺序 delete → update → insert。 -- 模块元数据(`s_module` / `s_field` / `s_module_schema`)**不由后端接口下发**, - 前端从本地 schema JSON 解析,所以改元数据后无需重启后端。 - -## 编译 / 测试 fms-api(本机 mvn 入口全是坏的,用这条) - -`fms-api/start.cmd` 里的 `JAVA_HOME=D:\devtool\jdk\jdk-21` 与 -`MAVEN_HOME=D:\devtool\maven\apache-maven-3.8.9` **两个路径都不存在** -(实际是 `D:\devtool\jdk\jdk21`、`D:\devtool\maven`),照抄必失败。 -`/d/devtool/maven/bin/mvn`(shell 脚本)在 Git Bash 下报 -`找不到或无法加载主类 org.codehaus.plexus.classworlds.launcher.Launcher`; -从 Bash 调 `cmd.exe` 被安全策略拦;另一个 shell 工具的输出不回流(得写文件再读)。 - -**唯一可行做法:直接用 JDK 拉起 Maven 的 classworlds launcher。** - -```bash -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`**(settings.xml 里改过 localRepository),不是 `~/.m2`。 -- 新依赖要联网首次下载,去掉 `-o` 即可(`repo1.maven.org` 可达)。 -- 中文乱码时输出里加 `grep -a`。 -- `target/` 被 git 跟踪,编译后会有一堆 `.class` / surefire 报告显示为 modified,属正常噪音。 - -**既有基线失败(不是自己引入的,别去修)**: -- `OrgDataSourceFactoryTests.databaseConnectionFailureDoesNotRetry` - (断言 Druid `getConnectionErrorRetryAttempts()==0`,实测 1)。 -- 前端 `tests/unit/fms-module-list-page.spec.js` 里所有引用 `.filter-drawer` / - `.filter-row__*` 的用例 —— 这些类名在 `src/` 里**已不存在**,是高级查询面板改版后没更新的过期断言。 - -## 导出功能(P0 已落地) - -- 入口:`POST /api/export/list`(`ExportController`),**返回文件流**,不是 ApiResponse。 -- 分层:`SqlPermissionService`(动作权限 + 字段白名单 + 数据范围)→ `ExportSchemaService` - (按 moduleId 读 s_module + s_field + s_module_schema.view,**与列表同源推导导出列**)→ - `ExportService`(拼 SQL、SQL 层聚合合计、JDBC 游标 + Fesod 流式写)→ - `ExportWorkbookWriter`(表头/明细/合计 → xlsx)→ `ExportHeadBuilder`(表头树 → `List>`) - + `ExportStyleHandler`(三种样式 + 逐列列宽)。 -- 关键约束:**导出列不由前端传**,否则字段权限(`s_user_field_power.b_export`)形同虚设; - **所有校验必须在写 `getOutputStream()` 之前完成**,且游标要「先 `executeQuery()` 再设响应头」, - 否则 SQL 报错时用户拿到的是半截文件而不是 JSON 错误。 -- 依赖:**`org.apache.fesod:fesod-sheet:2.0.2-incubating`**(Apache Fesod Incubating = EasyExcel 的 - Apache 续作,包名 `org.apache.fesod.sheet.*`,入口类 `FesodSheet`,API 与 EasyExcel 一致, - 自带 POI 5.5.1,**不要再单独引 POI**)。多级表头合并用 `automaticMergeHead(true)`, - 各列标题层数可不同(Fesod 底部对齐补空,列名落在最后一行)。 - **两个 Fesod 行为坑**:① 0 行导出必须仍调用一次 `write`(哪怕传空集合),否则生成的 xlsx - **没有任何工作表**;② 合计行是拼出来的数据行,「合计」标签要自己构造(落在第一个无聚合值的列)。 -- 合计行只在 view schema 的列节点带 `summary: sum|count` 时才出现;**目前没有任何模块配了它**, - 所以线上看不到合计行(多级表头同理,没有模块配 `type:group`)。 -- 前端:`services/exportService.js` + `FmsModuleListPage` 工具栏「导出」按钮; - `services/http.js` 的响应拦截器加了 blob 直通分支(下载类响应要能读 headers)。 -- 权限现状:`s_power` 只有 2 行(`s_i18n_type`),`s_user_field_power` / `s_user_data_power` - 都是空表。非超管走白名单语义会被拒(「没有导出权限」),**g3soft 提权可直接用**。 +`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 可直接用。 diff --git a/code/fms/FMS导出功能设计与进度.md b/code/fms/FMS导出功能设计与进度.md deleted file mode 100644 index 945f6155..00000000 --- a/code/fms/FMS导出功能设计与进度.md +++ /dev/null @@ -1,353 +0,0 @@ -# FMS 导出功能 · 设计与进度 - -> 状态:**P0 + 批量导出已完成并验证通过;剩余见第 6 节。** -> 最后更新:2026-09-16 - ---- - -## 0. 一句话现状 - -**导出**:列表工具栏的图标按钮,一键走人 —— 列与条件完全沿用列表,后端按模块元数据推导导出列、 -套字段权限与数据范围、流式生成 xlsx 返回下载。 - -**批量导出**:「更多操作 → 批量导出」打开弹窗,可选列(并能保存成个人偏好)、 -按条数区间导出、或每 N 条分卷打包成 zip。条件沿用列表当前条件,弹窗里不重做一套条件表单。 - -**合计行与多级表头**:后端已支持(读 view schema 的 `summary` 与 `type:'group'`), -但没有任何模块配置过,所以界面上还看不到(用户已明确「以后再说」)。 - -> ⚠️ **需要澄清一点(与最初讨论不一致)**:「导出」按钮导的**不是当前页,而是当前查询条件下的全部行**。 -> 后端只在传了 `rangeStart` / `rangeEnd` 时才加 `OFFSET/FETCH`,不传就是不限行数。 -> 集成测试已断言「导出行数 == 模块视图总行数」(`b_con_type` 8 行 → 8 行数据)。 -> 于是「批量导出」的差异在**选列 / 区间 / 分卷 / 保存导出列 / 条数预览**,数据范围上两者一样。 -> 若希望普通导出只导当前页,前端补传当前页区间即可(后端参数已就绪),是一行改动 —— 待定,见第 6.1 节。 - ---- - -## 1. 已完成(P0) - -### 1.1 后端 - -| 文件 | 职责 | -| --- | --- | -| `fms-api/.../service/SqlPermissionService.java` | **后端 SQL 权限出口**,三层:动作权限 / 字段白名单 / 数据范围。这是全项目第一个实现,后续列表查询接入时复用同一套判定 | -| `fms-api/.../service/ExportSchemaService.java` | 按 `moduleId` 读 `s_module` + `s_field` + `s_module_schema.view`,还原导出列与表头树 | -| `fms-api/.../service/ExportService.java` | 拼 SQL、SQL 层聚合合计、JDBC 游标 + Fesod 流式写出、并发闸门 | -| `fms-api/.../service/ExportWorkbookWriter.java` | 表头 + 明细(分批)+ 合计行 → xlsx;行来源抽象成 `RowSource`,所以能脱离数据库单测 | -| `fms-api/.../service/ExportHeadBuilder.java` | view schema 表头树 → Fesod 的 `List>` 表头结构 | -| `fms-api/.../service/ExportStyleHandler.java` | 表头 / 数据行 / 合计行三种样式 + 逐列列宽(`CellWriteHandler`) | -| `fms-api/.../controller/ExportController.java` | `POST /api/export/list`(文件流)与 `POST /api/export/count`(行数) | -| `src/test/.../ExportServiceTests.java` | 14 项:SQL 拼装、权限条件组合、合计 SQL、表头结构 | -| `src/test/.../ExportWorkbookWriterTests.java` | 6 项:表头 / 明细 / 合计 → xlsx 的写出(含分组表头与多批行数) | -| `src/test/.../ExportServiceIntegrationTests.java` | 5 项:连真实库端到端导出、计数、分卷 zip | - -改动: -- `pom.xml` 新增 `org.apache.fesod:fesod-sheet:2.0.2-incubating`(Apache Fesod Incubating,EasyExcel 的 Apache 续作); -- `DbUtils` 新增 `public String buildSafeOrderBy(String)`(原私有 `buildPageOrderBy` 的薄包装,导出复用它做排序校验,不另写一套)。 - -### 1.2 前端 - -| 文件 | 说明 | -| --- | --- | -| `fms-vue/src/services/exportService.js` | 新增。`exportListApi`(blob 下载 + `filename*` 解析 + 错误 JSON 识别)、`countExportApi` | -| `fms-vue/src/services/http.js` | 响应拦截器加 blob 直通分支(下载类响应需要读 headers) | -| `fms-vue/src/components/fms-module-list/FmsExportDialog.vue` | 新增。批量导出弹窗:选列 / 三种导出方式 / 保存导出列 | -| `fms-vue/src/components/fms-module-list/FmsModuleListPage.vue` | 工具栏「导出」图标按钮 + `handleExport()`;抽出 `buildSearchCondition()` 供取数与导出共用;「更多操作 → 批量导出」接线;个人导出列的读写 | -| `fms-vue/src/components/fms-module-list/useModulePref.js` | `loadModulePref` / `saveModulePref` 加 `schemaType` 参数(默认仍是 `query`),供「我的导出列」复用 | -| `fms-vue/tests/unit/fms-export-dialog.spec.js` | 新增。8 项:默认全选、取消勾选后的列、区间 / 分卷参数、非法输入不发起请求、保存 / 恢复导出列 | - ---- - -## 2. 接口契约 - -``` -POST /api/export/list → 文件流(xlsx;分卷时是 zip) -POST /api/export/count → ApiResponse<{ total }> -``` - -两个接口吃同一套参数,`count` 忽略导出方式相关参数。 - -| 参数 | 必填 | 说明 | -| --- | --- | --- | -| `moduleId` | ✅ | 模块编码。后端据此读元数据与权限,**不接受前端传列清单** | -| `searchCondition` | | 查询条件(不含 WHERE),与列表当前条件一致;后端再 AND 数据范围 | -| `orderBy` | | 排序子句;缺省用模块默认排序 `s_module.b_order_sql` | -| `rowKeys` | | 勾选行主键值数组;非空时只导出这些行(用 `?` 占位绑定,不拼字面量) | -| `keyField` | | 勾选行主键列;缺省用 `s_module.b_key_field` | -| `columns` | | 勾选导出的字段;省略 = 沿用列表全部列(批量导出用) | -| `rangeStart` / `rangeEnd` | | 按条数区间(1 基,闭区间),两者同时给出才生效,走 `OFFSET/FETCH`。**不传即导出全部行** | -| `volumeSize` | | 分卷行数:> 0 时返回 zip(每卷一个 xlsx,命名 `{模块名}_{起}-{止}.xlsx`)。与区间互斥,逐卷不输出合计行 | -| `withSummary` | | 是否输出合计行,默认 `true` | - -响应: -- 成功:文件流,`Content-Type` 为 `application/vnd...spreadsheetml.sheet`(分卷时 `application/zip`), - 文件名在 `Content-Disposition: attachment; filename*=UTF-8''{urlencoded}`; -- 失败:**HTTP 200 + JSON 业务错误**(`{code, message, data}`)。前端必须按 `content-type` 区分, - 否则用户会下到一个内容其实是 JSON 的 `.xlsx`。 - -`POST /api/export/count` 单独存在(而不是复用列表分页的 total)的原因是: -它必须走**与导出完全相同**的权限链路(动作权限 + 字段裁剪 + 数据范围), -列表分页的 total 不受数据范围约束,配了数据范围时两者会对不上。 - ---- - -## 3. 设计决策与理由 - -1. **后端是唯一出口,且导出列由后端自己读元数据推导。** - 前端只传 `moduleId`。理由是字段权限(`s_user_field_power.b_export`)只能在后端执行, - 若信任前端传来的列清单,改一下 payload 就能导出工资列。副产品是「普通导出与列表一致」 - 成了同源推导的必然结果,而不是两边各自实现再祈祷一致。 - -2. **`ExportSchemaService` 与列表用同一套还原顺序**(`s_module` → `s_field`(`b_canuse=1`、 - 标识符合法)→ `s_module_schema.view` 按文档顺序取叶子、递归展开分组 → 过滤 `visible=false` - 与未知字段)。前端对应实现是 `schemaRender.js` 的 `buildListConfig`。 - -3. **所有校验必须在写 `response.getOutputStream()` 之前完成。** - 一旦开始写流就不能再改 header 和状态码,失败只会给用户一个损坏文件且看不到错误。 - `ExportService.export()` 的顺序是:参数校验 → 动作权限 → 列推导 + 字段裁剪 → - 数据范围 → SQL 校验 → 合计 → **`executeQuery()`** → 才设响应头并开始写流。 - 注意游标是「先查后设头」:这样 SQL 本身报错(列名写错、语法问题)仍然能返回 JSON; - 代价是一旦开始写流,中途的数据库异常无法再以 JSON 回传 —— 流式导出的固有权衡。 - 集成测试专门验证了这条(模块不存在时响应体为空)。 - -4. **流式**:JDBC `TYPE_FORWARD_ONLY` + `setFetchSize(500)` 配 Fesod 分批 `write`(底层是 SXSSF, - 内存占用与总行数无关;每批 1000 行)。合计用**独立聚合 SQL**(`SUM()` / `COUNT()`), - 不把明细拉到内存 reduce。 - -5. **用 Apache Fesod (Incubating)**(`org.apache.fesod:fesod-sheet:2.0.2-incubating`,EasyExcel 的 Apache 续作, - 自带 POI 5.5.1 / Commons CSV / Ehcache,**不要再单独引 POI**)。 - 最初一版是自己用 POI SXSSF 手写,后按需求换成 Fesod:多级表头合并、样式、列宽都有现成能力, - 少维护一层。**注意 Fesod 的 API 与 EasyExcel 一致**(入口类 `FesodSheet`,同时保留了 - `EasyExcel` / `FastExcel` 兼容类名),所以旧资料可以直接参考。 - -6. **「先不做行数上限」的替代保护**:全局同时最多 2 个导出任务、同一用户同时 1 个、 - 语句走 Druid 超时、流式写。若将来要加硬上限,位置在 `ExportService.export()` 开头的校验段。 - -7. **多级表头交给 Fesod**:`ExportHeadBuilder` 只把表头树转成 `List>` - (外层 = 叶子列,内层 = 该列自上而下的标题),合并由 `automaticMergeHead(true)` 负责。 - 实测**各列内层长度可以不同**(顶层叶子只有 1 层),Fesod 会底部对齐补空 —— - 列名一定落在表头最后一行,分组标题按相邻同值横向合并,正是要的形态。 - (第一版自己手写合并区域时踩过「分组横向 + 叶子纵向区域重叠」的坑,换库后这段代码直接删掉了。) - ---- - -## 4. 怎么验证 - -### 4.1 本机编译 / 测试(**注意:`start.cmd` 是坏的**) - -`fms-api/start.cmd` 里的 `JAVA_HOME=D:\devtool\jdk\jdk-21` 与 -`MAVEN_HOME=D:\devtool\maven\apache-maven-3.8.9` **两个路径都不存在** -(实际是 `D:\devtool\jdk\jdk21` 与 `D:\devtool\maven`)。 -`bin/mvn` 这个 shell 脚本在 Git Bash 下会报「找不到或无法加载主类 -`org.codehaus.plexus.classworlds.launcher.Launcher`」。 - -可行的做法是直接用 JDK 拉起 Maven 的 classworlds launcher: - -```bash -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=ExportServiceTests test -``` - -- 本地仓库是 `D:\data\maven`(`settings.xml` 改过 `localRepository`),不是 `~/.m2`; -- 新依赖首次要联网,去掉 `-o`; -- 输出中文乱码时加 `grep -a`。 - -### 4.2 本次验证结果 - -- `ExportServiceTests`:14 passed。 -- `ExportWorkbookWriterTests`:6 passed。覆盖单行表头、**分组表头与顶层叶子混排**、三层表头逐级合并、 - 列宽换算、0 行导出仍出表头、2500 行跨批次全部写出。 -- `ExportServiceIntegrationTests`:5 passed。导出 `b_con_type`(箱型管理,8 行)→ - 生成的字节流能被 POI 解析、sheet 名正确、表头与数据行齐全且行数等于视图总行数; - 勾选 1 行 → 结果只有 1 行数据;**计数**与视图总行数一致、条件参与计数; - **分卷**(每卷 3 行)→ zip 内 3 个文件、行数 3/3/2;模块不存在 → 抛业务异常且响应体为空。 -- 前端 `tests/unit/fms-export-dialog.spec.js`:8 passed(默认全选、取消勾选后的列、 - 区间 / 分卷参数、非法区间与空选不发请求、保存 / 恢复导出列)。 -- 后端全量 54 项,唯一失败 `OrgDataSourceFactoryTests.databaseConnectionFailureDoesNotRetry` - 是**既有基线**(断言 Druid `getConnectionErrorRetryAttempts()==0`,实测 1,与本次改动无关)。 -- 前端改动文件 oxlint 0 error。既有基线失败:`fms-module-list-page.spec.js` 3 项 - (断言的 `.filter-drawer` / `.filter-row__*` 在 `src/` 里已不存在)、 - `module-pref.spec.js` 2 项(断言的默认布局「非 quick 字段进 advanced」与实现的 - 「非 hidden 即 quick」不一致,测试未跟上实现)。 - ---- - -## 5. 数据库现状(决定了「为什么现在看不到合计行」) - -| 表 | 行数 | 影响 | -| --- | --- | --- | -| `s_power` | 2(都是 `s_i18n_type`) | 除超管外,任何模块都没有 `action.*.export` 权限点 → 非超管导出会被拒(「没有导出权限」) | -| `s_user_power` | 0 | 同上,白名单语义下未授权即拒绝 | -| `s_user_field_power` | 0 | 无记录 = 可查看/可查询/可导出(14.3 默认行为),当前无字段被裁剪 | -| `s_user_data_power` | 0 | 无生效规则 = 无数据范围限制 | -| `s_user_module_pref` | 0 | 个人层覆盖(P1 的「保存导出列」会写这里,`b_schema_type='export'`) | -| `s_module_schema` | view 71 / edit 68 / query 19 | **没有任何 view schema 配了 `summary` 或 `type:'group'`** → 合计行与分组表头不会出现 | - ---- - -## 6. 未完成 - -### 6.1 P1(建议按此顺序) - -0. ~~**普通导出语义**~~ **已改为当前页**:无勾选时导出当前页(前端补传 `rangeStart/rangeEnd`), - 合计行即当页合计;有勾选时仍导出勾选行。批量导出(高级导出)按所选区间/分卷聚合合计。 - -1. ~~**合计行与分组表头的配置入口**~~ **已完成**:模块管理 → 列表配置 → 批量编辑已支持 - 「合计」列(无 / 求和 / 计数),写回 `s_module_schema.view` 的列节点 `summary`; - 列表页底部已显示两行合计(该页合计 + 总合计),导出文件也按范围输出对应合计行。 - 分组表头本来就能在画布里配,不需要新做,只要配了就会生效。 - -2. ~~**批量导出弹窗**~~ **已完成**(见第 7 节)。后端 `columns` / `rangeStart` / `rangeEnd` / - `volumeSize` 与「保存导出列」都已落地并验证。 - -3. **前端按 `canPower(moduleId, 'export')` 隐藏导出入口** - - 现在没做,无权限用户会看到入口、点了才报错。后端已拦截,属于体验问题。 - - 做之前要确认 `canPower` 对「数据模块编码」的判定是否符合预期 - (`canPower` 内部先查 `canAccess(scopeCode)`,而页面路由的 `meta.moduleId` 与 - `dataModule.b_id` 未必是同一个编码,存在把有权限的用户也隐藏掉的风险)。 - -4. **字段权限的前端缺口**:列表目前只用 `b_query` 过滤查询条件, - **没有用 `b_view` 过滤列**(`FmsModuleListPage` 里 `activeFields = fields` 是全量)。 - 设计文档 14.3 要求 `b_view=0` 的字段在列表/表单/查询/导出中统一移除。 - 导出这一侧已经做了,列表那侧没做,于是会出现「列表能看到某列、导出里没有」的不一致。 - 当前 `s_user_field_power` 是空表所以无感,但应该补。 - -5. **批量导出的条件编辑**:目前弹窗里条件只读(显示「沿用列表当前条件」)。 - 若要在弹窗内改条件,正确做法是复用现有高级查询面板,而不是另写一套字段表单 —— 见第 7 节。 - -### 6.2 P2 - -- CSV 导出(`format` 参数目前只认 xlsx,未实现 CSV); -- 分卷 zip(`volumeSize` 未实现,接口里也没有这个参数); -- 模板导出(旧系统有 `s_exceltpl` / `s_exceltpl_sub` 那套自定义 Excel 模板,业务真需要再单独立项)。 - -### 6.3 已知口径 / 取舍 - -- **列宽换算**:`view` 的 `width` 是像素,Excel 列宽单位是字符数,换算为 `px / 8`, - 夹在 `[8, 60]` 之间,无配置时兜底 16。 -- **单元格格式**:目前不读 `view` 节点的 `format`(如 `money`),数字按 Excel 原生数值写入, - 没有设置数字格式。要按业务格式显示需要后续补。 -- **表头文案**:取 `s_field.b_name`,**未做多语言**(前端列表是走 i18n store 的)。 - 导出跟当前界面语言一致需要后续补。 -- **勾选行跨页**:`rowKeys` 是主键值数组,跨页勾选可用;但一次勾太多会撑大 `IN (...)` 的占位数量, - 极端情况下应改成分批或临时表。 - ---- - -## 7. 批量导出弹窗(已实现) - -入口:列表工具栏「更多操作 → 批量导出」。组件 `components/fms-module-list/FmsExportDialog.vue`。 - -参考 `D:\workspace\code\container-ocr\ocr-vue` 的 `high-download-dialog.vue`,但做了几处调整: - -- **界面刻意极简**:只留「导出列」「导出方式」两块 + 底部「共 N 条 / 重置 / 下载」。 - 不放任何说明性文字(模式说明、条件说明、分组表头与合计行的说明全部删掉)—— - 说明写在本文件与代码注释里,不占用界面。 -- **导出列是竖排列表,可拖拽排序**(用项目已有的 `vuedraggable`,与设计编辑器同一套库)。 - 显示顺序即导出顺序。有个人保存的列时:已保存的按保存顺序排前面并勾上,其余列补在后面且不勾选。 -- **条件区不另写一套字段表单**:弹窗里条件只读,要改条件先在列表上改。 - 新系统字段是元数据驱动的,`ocr-vue` 那种硬编码几个字段的做法不可能覆盖; - 将来若要支持弹窗内改条件,应复用现有高级查询面板,而不是第三套 UI。 -- 顶部「共 N 条数据」调 `POST /api/export/count`,**与导出走同一条权限链路**(不是列表分页的 total); -- **导出方式**用 `Segmented` 三选一:全部 / 按区间 / 分卷(每 N 条一个文件,后端打包 zip)。 - 布局是「标题 + Segmented 同一行,参数行单独一行、落在 Segmented 下方并左对齐」: - `.export-dialog__mode` 是两列 grid(左标题 / 右控件),三个 grid 项各居自己所在行, - 所以标题不会被参数行拉低,区间与分卷的参数出现位置也完全一致。 - 底部主按钮文案是**导出**(不是「下载」)。 - -列区只有两个动作: -- **全选** —— 候选集就是列表可见列,所以「全选」等价于「回到列表列」,顺手清掉个人覆盖; -- **保存配置** —— 把当前勾选与顺序存成个人偏好。 - -导出列列表限高 340px 内滚动(超过就出现滚动条,弹窗本身不会被撑高)。 - -个人导出列存 `s_user_module_pref`(`b_schema_type='export'`,JSON `{ schemaVersion, columns: [字段编码] }`, -数组顺序即导出顺序),沿用《FMS新系统核心表结构设计》第 7 节的「个人层只存增量、 -恢复默认即删除覆盖」规则。 - -分组表头与合计行**不放进弹窗**,它们属于 `view` schema(列表与导出共用)。 -分卷导出逐卷不输出合计行(逐卷合计没有业务意义)。 - -**与旧系统 `g3hd` 的关键差别**:那边是「前端按 10000 行切段 → N 次请求 → 前端 JSZip 打包」, -这里是「一次请求,后端切卷打包」。前端不用维护 N 次往返的进度与失败重试, -也不用同时持有 N 个 blob。代价是单卷内容在内存里渲染(受 `volumeSize` 约束,所以是有界内存, -不是流式),卷大小要设得合理。 - ---- - -## 8. 附:本次顺带确认的元数据事实 - -- `s_module.b_id` 是模块编码,`b_view_table` 是查询视图名,`b_key_field` 是主键列, - `b_order_sql` 是默认排序(裸 SQL,如 `b_xh ASC, b_id ASC`)。 -- `s_module_schema.view` 的 JSON 形状:`{"schemaVersion":1,"columns":[{"field":"b_name","width":150}, - {"type":"group","title":"基本信息","children":[...]}]}`;列节点可选 `visible` / `summary`。 -- 权限编码格式:`menu.{菜单}` / `module.{模块}` / `action.{模块}.{动作}`, - 由 `s_power.b_object_type` + `b_object_id` + `b_action` 派生(`ModulePowerPanel.powerCodeOf`)。 -- 旧系统参考实现里**不要照搬**的三处: - `select identity(int,1,1),* into #tmptj` 取行号(并发导出会撑爆 tempdb,改用 `ROW_NUMBER()` 或 `OFFSET/FETCH`); - 前端 ExcelJS 生成(新系统列表是分页的,只能导当前 20 条); - `g3hd` 的 `handleDownloadZip` 存的是 `mx_mapfilename` 却 `revokeObjectURL(file.url)`,blob URL 从未释放。 - ---- - -## 9. 实施过程中踩到的坑(供后续参考) - -### 9.1 环境(最耗时的部分) - -- `fms-api/start.cmd` 里的 `JAVA_HOME` / `MAVEN_HOME` **两个路径都不存在**,照抄必失败; - `bin/mvn` 在 Git Bash 下报 classworlds 找不到;从 Bash 调 `cmd.exe` 被安全策略拦; - 另一个 shell 工具的输出不回流(只能写文件再读)。最终靠 JDK 直拉 classworlds launcher 跑通(见 4.1)。 -- 本地 Maven 仓库是 `D:\data\maven` 而不是 `~/.m2`,查「依赖有没有缓存」时别找错地方。 -- 后端 8088 上跑的是**改动前**的实例,没有导出接口。想走 HTTP 端到端验证就得重启它(会打断 - 正在开发的人),所以改用 `@SpringBootTest` 直连真实库做集成测试 —— 这条路反而更好,可重复。 -- 想开浏览器截图验证前端,但登不进去:`g3soft` 空密码返回 401,密码非空串。 - 没有为了验证去读用户密码,改成挂载组件断言 DOM 结构。 - -### 9.2 技术选型 - -- 第一版为了控制表头合并、逐列列宽、合计行样式三处,绕开 EasyExcel 4.0.x 的 `CellWriteHandler` - 签名变动,直接用 POI SXSSF 手写;后按要求改为 **Apache Fesod (Incubating) 2.0.2**(EasyExcel 的 Apache 续作)。 - 迁移代价很小:表头合并改用 `automaticMergeHead(true)`,手写的 `ExportHeaderWriter` 直接删除; - 样式与列宽收进一个 `CellWriteHandler`。 -- 选型时**不要靠记忆猜 API**:Fesod 的类名/包名与 EasyExcel 不同(`org.apache.fesod.sheet.*`), - 用 `jar tf` + `javap` 直接看下载下来的 jar 最省事。 - -### 9.3 实现 - -- 首次编译 7 个类型错误:`Map.get()` 返回 `Object`,而工具方法只接受 `String`。 -- **排序校验的位置一开始找错了**:`buildSelectSql` 信任传入的 `orderSql`(真正的校验在 - `resolveOrderSql` 里)。测试改成打真正的校验口,顺带把 `resolveOrderSql` 从 private 放开为包级可见。 -- 手写合并的那一版,多级表头规则我第一版测试预期就是错的:以为叶子要纵向合并到表头最后一行, - 实际上两级表头里叶子已经在最后一行、再合并就会和分组的横向合并区域**重叠** - (POI 会抛异常或静默丢内容)。换成 Fesod 后这类问题不再需要自己保证, - 但**表头结构测试仍然保留**(`ExportHeadBuilder` 的路径展开 + 写出后的表头位置断言)。 -- **换库时被自己的测试抓到两个真问题**(这两条都是 Fesod 行为,不是笔误): - 1. **0 行导出会生成「没有任何工作表」的空 xlsx** —— Fesod 只在 `write` 被调用时才创建工作表, - 所以最后一批即使为空也必须调用一次。已修,并加了「0 行仍出表头」的用例。 - 2. 合计行的「合计」标签原来由 POI 版本写在首列;换成 Fesod 后合计行是拼出来的数据行, - 需要显式构造:标签落在**第一个没有聚合值的列**(正常就是首列),所有列都有聚合值时不加标签。 -- **写测试时又被自己抓到一个错**:集成测试里用 `b_id = N'绝不存在的值'` 构造「查不到」的条件, - 结果 SQL Server 报 `Error converting data type nvarchar to bigint` —— - `b_con_type.b_id` 是 **bigint**(新系统业务表用雪花 ID),拿字符串去比会转换失败。 - 教训:**测试里构造条件前先确认列的真实类型**,别照抄字典表(`s_module.b_id` 是 varchar)的习惯。 -- 二进制响应**不要调 `response.setCharacterEncoding`**:会让 content-type 变成 - `application/zip;charset=UTF-8`,前端按 `content-type` 判断文件类型时会受影响。 - -### 9.4 数据与权限现实 - -- `s_power` 只有 2 行(都是 `s_i18n_type`),`s_user_field_power` / `s_user_data_power` 都是空表。 - 也就是说**这套权限体系实际上还没启用**,导出成了第一个真正执行它的功能 —— 因此除了 `g3soft`, - 其他账号现在都会被拒。这不是 bug,是白名单语义下的必然结果。 -- 没有任何模块的 view schema 配过 `summary` 或 `type:'group'`,所以合计行与多级表头写完也看不到。 - -### 9.5 顺带发现的既有问题(不是本次引入) - -- 列表只用 `b_query` 过滤查询条件,**没有用 `b_view` 过滤列**,与导出的裁剪口径不一致(见 6.1 第 4 项)。 -- `fms-api/target/` 被 git 跟踪,编译后一堆 `.class` 与 surefire 报告会显示为 modified。 -- 既有测试基线失败:后端 `OrgDataSourceFactoryTests` 1 项;前端 `fms-module-list-page.spec.js` - 3 项(断言的 `.filter-drawer` / `.filter-row__*` 在 `src/` 里已不存在)。 diff --git a/code/fms/FMS新系统核心表结构设计.md b/code/fms/FMS新系统核心表结构设计.md index c62b32db..80e65966 100644 --- a/code/fms/FMS新系统核心表结构设计.md +++ b/code/fms/FMS新系统核心表结构设计.md @@ -370,8 +370,9 @@ query }, { "type": "group", + "id": "group1", "title": "费用信息", - "i18n": "group.cw_fee.amount", + "i18n": "group.cw_fee.view.group1", "children": [ { "field": "mx_amount", "width": 120, "format": "money" }, { "field": "mx_currency", "width": 90 } @@ -389,8 +390,9 @@ query "children": [ { "type": "group", + "id": "group1", "title": "基础信息", - "i18n": "group.sea_main.basic", + "i18n": "group.sea_main.edit.group1", "children": [ { "type": "row", @@ -442,6 +444,20 @@ query } ``` +### 6.4 分组节点 id 与资源键 + +`view`、`edit` 的 JSON 里,分组节点(`type: "group"`)用 `id` 表达稳定身份;层级由 `children` 嵌套表达,顺序由数组顺序表达,分组不建独立表(见本节开头的 `s_field_group`)。 + +```text +"id": "group1" 按 schema 类型独立编号(view 与 edit 各自从 group1 开始) +``` + +- **唯一范围**:分组 id 只在单个 `b_schema_json` 内唯一。列表的 `group1` 与表单的 `group1` 是两处不同分组,互不引用,因此也不做模块内跨 schema 全局唯一。 +- **稳定优先**:编号一旦分配不再重排,删除分组留下的空洞允许存在(重排会改身份,进而改资源键)。 +- **取号规则**:新建分组取该配置内已用编号里最小的未用正整数;同时保留 `s_i18n` 中该模块同 schema 已存在的分组键编号,避免新分组捡到已删除分组的编号、连带继承其残留译文。 +- **老数据兼容**:`id` 缺失或重复的节点由归一化补齐(先序、取最小可用编号),非 `group{n}` 的自定义 id 视为已占用并原样保留。归一化必须是确定性纯函数——加载后要与基线比较,若补号依赖外部上下文,同一份数据两次归一化结果会不同而被误判为未保存改动。 +- **资源键**:分组标题的资源键为 `group.{模块编码}.{schema 类型}.{分组编号}`(如 `group.cw_fee.view.group1`),由 id 派生而非人工填写;节点上的 `i18n` 手填优先,未填时按此规则派生,使分组标题无需手工登记即进入多语言受管清单。schema 类型必须入键,否则列表与表单的同号分组会互相覆盖译文。 + ## 7. 用户个性化(个人层) 个人配置是两层继承的最上层,只保存相对系统默认(`s_module_schema`)的偏离量,不复制完整模块配置。与 `s_module_schema` 统一使用 `b_schema_type` + `b_schema_json`,按 schema 类型分别存一条。 @@ -623,11 +639,13 @@ create index ix_s_i18n_locale | `module.` | 模块名称或模块业务文案 | `module.sea.name` | | `menu.` | 菜单名称 | `menu.main.title` | | `field.` | 字段名称 | `field.sea.b_no` | -| `group.` | 列表/表单布局分组标题 | `group.sea.basic` | +| `group.` | 列表/表单布局分组标题 | `group.sea.view.group1` | | `action.` | 动作动词(按钮 + 权限共用) | `action.insert` | 资源键一旦投入使用原则上不可修改;模块、菜单或字段删除时,由业务层根据已知资源键清理对应翻译行。 +`module.*`、`menu.*`、`field.*`、`action.*` 由业务编码派生,不需要人工填写;`group.*` 的编号段取自界面配置里分组节点的 `id`,未手填节点 `i18n` 时同样由 id 派生,规则见 6.4 节。模块管理页登记的分组译文在表单渲染时按同一规则解析(节点缺失 id 的老数据由归一化补齐后同样能取到译文)。 + `action.*` 保存通用的动作动词资源键(例如 `action.insert`、`action.update`、`action.audit`),按钮文案和权限点名称共用同一翻译,同一动词只维护一份;动作动词是通用词,不按模块或对象分段。 ## 12. 日志 diff --git a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/config/JwtAuthFilter.java b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/config/JwtAuthFilter.java index 8c79df3f..8d91a2af 100644 --- a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/config/JwtAuthFilter.java +++ b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/config/JwtAuthFilter.java @@ -56,15 +56,7 @@ public class JwtAuthFilter extends OncePerRequestFilter { return; } - if (!activeSessionRegistry.hasActiveSession(claims.orgId(), claims.userId())) { - writeUnauthorized( - response, - ApiResponse.AUTH_ERROR_CODE, - "未登录或登录已失效" - ); - return; - } - + // JWT 已通过签名和到期校验;重启后缺失的内存会话由首个有效请求恢复。 if (!activeSessionRegistry.isActive( claims.orgId(), claims.userId(), diff --git a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/controller/ExportController.java b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/controller/ExportController.java index eb6c5979..b49f9039 100644 --- a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/controller/ExportController.java +++ b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/controller/ExportController.java @@ -1,6 +1,7 @@ package cn.g3soft.fmsapi.controller; import cn.g3soft.fmsapi.service.ExportService; +import cn.g3soft.fmsapi.exception.BusinessException; import cn.g3soft.fmsapi.utils.ApiResponse; import cn.g3soft.fmsapi.utils.ParamUtils; import jakarta.annotation.Resource; @@ -11,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; +import java.math.BigDecimal; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -87,7 +89,12 @@ public class ExportController { private Integer readInteger(Map params, String field) { Number number = ParamUtils.get(params, field, Number.class); - return number == null ? null : number.intValue(); + if (number == null) return null; + try { + return new BigDecimal(number.toString()).intValueExact(); + } catch (ArithmeticException | NumberFormatException exception) { + throw new BusinessException(field + " 必须是有效整数"); + } } /** 缺省输出合计行 */ diff --git a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java index 91e532ff..8d667395 100644 --- a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java +++ b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java @@ -14,15 +14,17 @@ public class ActiveSessionRegistry { activeSessions.put(new AccountKey(orgId, userId), sessionId); } + /** + * 必须在 JWT 签名和有效期校验通过后调用。 + * 重启后由首个有效请求恢复账号会话,不持久化;重启前的互踢历史也随之清空。 + */ public boolean isActive(String orgId, String userId, String sessionId) { if (sessionId == null || sessionId.isBlank()) { return false; } - return sessionId.equals(activeSessions.get(new AccountKey(orgId, userId))); - } - - public boolean hasActiveSession(String orgId, String userId) { - return activeSessions.containsKey(new AccountKey(orgId, userId)); + // 原子登记:并发首次请求只能有一个会话胜出,也不会覆盖并发的新登录。 + String activeSession = activeSessions.putIfAbsent(new AccountKey(orgId, userId), sessionId); + return activeSession == null || sessionId.equals(activeSession); } private record AccountKey(String orgId, String userId) { diff --git a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/ExportSchemaService.java b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/ExportSchemaService.java index 2276c8eb..b07fbfdc 100644 --- a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/ExportSchemaService.java +++ b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/ExportSchemaService.java @@ -156,6 +156,9 @@ public class ExportSchemaService { ) { Set selected = selectedColumns == null ? null : new LinkedHashSet<>(selectedColumns); List nodes = filterNodes(schema.nodes(), deniedFields, selected); + if (selected != null) { + nodes = orderNodes(nodes, selected); + } if (nodes.isEmpty()) { throw new BusinessException("没有可导出的列"); } @@ -198,6 +201,45 @@ public class ExportSchemaService { return kept; } + /** 按用户选择的叶子顺序重建表头;跨组拖拽时分开不相邻的同组列,保留每列的分组路径。 */ + private List orderNodes(List nodes, Set selected) { + Map> paths = new LinkedHashMap<>(); + collectPaths(nodes, List.of(), paths); + List ordered = new ArrayList<>(); + List previous = List.of(); + List> levels = new ArrayList<>(); + levels.add(ordered); + for (String field : selected) { + List path = paths.get(field); + if (path == null) continue; + int shared = 0; + while (shared < path.size() - 1 && shared < previous.size() - 1 + && path.get(shared) == previous.get(shared)) { + shared++; + } + while (levels.size() > shared + 1) levels.remove(levels.size() - 1); + for (int depth = shared; depth < path.size() - 1; depth++) { + List children = new ArrayList<>(); + levels.get(depth).add(new ExportNode(path.get(depth).title(), null, children)); + levels.add(children); + } + levels.get(path.size() - 1).add(path.get(path.size() - 1)); + previous = path; + } + return ordered; + } + + private void collectPaths( + List nodes, List parents, Map> paths + ) { + for (ExportNode node : nodes) { + List path = new ArrayList<>(parents); + path.add(node); + if (node.isGroup()) collectPaths(node.children(), path, paths); + else if (node.column() != null) paths.putIfAbsent(node.column().field(), path); + } + } + /* ================= 元数据读取 ================= */ private Map loadFieldCaptions(Connection connection, String moduleId) throws SQLException { @@ -263,7 +305,7 @@ public class ExportSchemaService { continue; } String field = text(node.path("field").asString(null)); - if (field.isBlank() || !SAFE_IDENTIFIER.matcher(field).matches()) { + if (field.isBlank() || !SAFE_IDENTIFIER.matcher(field).matches() || !captions.containsKey(field)) { continue; } // 与列表一致:visible=false 的节点不出现在列表,也不出现在导出 diff --git a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/ExportService.java b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/ExportService.java index 5bca728c..c0ac013b 100644 --- a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/ExportService.java +++ b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/ExportService.java @@ -214,8 +214,8 @@ public class ExportService { ? loadTotals( prepared, whereParameters, - isRanged(request) ? request.rangeStart() : null, - isRanged(request) ? request.rangeEnd() : null + isRanged(request) ? request.rangeStart().longValue() : null, + isRanged(request) ? request.rangeEnd().longValue() : null ) : List.of(); @@ -267,9 +267,12 @@ public class ExportService { private boolean isVolumed(ExportRequest request) { Integer volumeSize = request.volumeSize(); - if (volumeSize == null || volumeSize <= 0) { + if (volumeSize == null) { return false; } + if (volumeSize <= 0) { + throw new BusinessException("每卷行数必须大于 0"); + } if (isRanged(request)) { throw new BusinessException("分卷导出与按区间导出不能同时使用"); } @@ -321,9 +324,12 @@ public class ExportService { ? schema.orderSql() : requestedOrderBy.trim(); if (orderBy == null || orderBy.isBlank()) { - return null; + orderBy = schema.keyField() == null ? null : schema.keyField() + " ASC"; + } else if (schema.keyField() != null) { + // 相同排序值以主键打破平局,明细、区间合计与各卷使用同一行顺序。 + orderBy += ", " + schema.keyField() + " ASC"; } - return dbUtils.buildSafeOrderBy(orderBy); + return orderBy == null ? null : dbUtils.buildSafeOrderBy(orderBy); } String buildSelectSql( @@ -370,10 +376,10 @@ public class ExportService { private boolean isRanged(ExportRequest request) { Integer start = request.rangeStart(); Integer end = request.rangeEnd(); - if (start == null || end == null) { + if (start == null && end == null) { return false; } - if (start < 1 || end < start) { + if (start == null || end == null || start < 1 || end < start) { throw new BusinessException("导出区间不正确"); } return true; @@ -387,8 +393,8 @@ public class ExportService { private List loadTotals( PreparedExport prepared, List whereParameters, - Integer rangeStart, - Integer rangeEnd + Long rangeStart, + Long rangeEnd ) { List columns = prepared.columns(); List indexes = summableIndexes(columns); @@ -412,16 +418,15 @@ public class ExportService { statement.setObject(index++, parameter); } if (ranged) { - statement.setLong(index++, (long) rangeStart - 1); - statement.setLong(index, (long) (rangeEnd - rangeStart + 1)); + // ROW_NUMBER 从 1 开始,BETWEEN 接收闭区间端点,不是 OFFSET/FETCH 的偏移和条数。 + statement.setLong(index++, rangeStart); + statement.setLong(index, rangeEnd); } try (ResultSet resultSet = statement.executeQuery()) { if (resultSet.next()) { for (int position = 0; position < indexes.size(); position++) { BigDecimal value = resultSet.getBigDecimal(position + 1); - if (value != null) { - totals.set(indexes.get(position), value); - } + totals.set(indexes.get(position), value == null ? BigDecimal.ZERO : value); } } } @@ -530,20 +535,24 @@ public class ExportService { dbUtils.validateReadOnlySql(selectSql); long volumeCount = total == 0 ? 1 : (total + volumeSize - 1) / volumeSize; + // 第一卷查询、合计和渲染成功后才开始响应,首卷失败仍能返回业务错误。 + byte[] firstVolume = renderVolume(prepared, request, selectSql, sheetName, 0, + (int) Math.min(volumeSize, total)); applyResponseHeader(response, prepared.schema().moduleName(), "zip", "application/zip"); - ZipOutputStream zip = new ZipOutputStream(response.getOutputStream(), StandardCharsets.UTF_8); - try { + try (ZipOutputStream zip = new ZipOutputStream(response.getOutputStream(), StandardCharsets.UTF_8)) { for (long index = 0; index < volumeCount; index++) { long offset = index * volumeSize; int count = (int) Math.min(volumeSize, Math.max(0, total - offset)); + byte[] content = index == 0 ? firstVolume + : renderVolume(prepared, request, selectSql, sheetName, offset, count); zip.putNextEntry(new ZipEntry(volumeEntryName(prepared.schema().moduleName(), offset, count))); - zip.write(renderVolume(prepared, request, selectSql, sheetName, offset, count)); + zip.write(content); + firstVolume = null; zip.closeEntry(); } zip.finish(); - } finally { - response.flushBuffer(); } + response.flushBuffer(); } /** 单卷内容渲染到内存:卷大小已限定行数,因此内存有界 */ @@ -558,7 +567,7 @@ public class ExportService { List whereParameters = bindWhereParameters(new ArrayList<>(), request); // 该卷的合计:与明细同一个行范围,口径与其它导出方式一致 List totals = request.withSummary() && count > 0 - ? loadTotals(prepared, whereParameters, (int) (offset + 1), (int) (offset + count)) + ? loadTotals(prepared, whereParameters, offset + 1, offset + count) : List.of(); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); if (count <= 0) { diff --git a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/ExportWorkbookWriter.java b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/ExportWorkbookWriter.java index 38b836e4..9fecb815 100644 --- a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/ExportWorkbookWriter.java +++ b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/ExportWorkbookWriter.java @@ -5,6 +5,7 @@ import cn.g3soft.fmsapi.service.ExportSchemaService.ExportNode; import org.apache.fesod.sheet.ExcelWriter; import org.apache.fesod.sheet.FesodSheet; import org.apache.fesod.sheet.write.metadata.WriteSheet; +import org.apache.poi.ss.util.WorkbookUtil; import java.io.IOException; import java.io.OutputStream; @@ -60,7 +61,7 @@ final class ExportWorkbookWriter { .registerWriteHandler(styleHandler) .build(); try { - WriteSheet sheet = FesodSheet.writerSheet(sheetName).build(); + WriteSheet sheet = FesodSheet.writerSheet(WorkbookUtil.createSafeSheetName(sheetName)).build(); int rowCount = 0; List> batch = new ArrayList<>(BATCH_SIZE); diff --git a/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/config/JwtAuthFilterTests.java b/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/config/JwtAuthFilterTests.java new file mode 100644 index 00000000..68dbb7ec --- /dev/null +++ b/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/config/JwtAuthFilterTests.java @@ -0,0 +1,124 @@ +package cn.g3soft.fmsapi.config; + +import cn.g3soft.fmsapi.database.OrgContext; +import cn.g3soft.fmsapi.service.ActiveSessionRegistry; +import cn.g3soft.fmsapi.utils.JwtUtils; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.FilterChain; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.util.ReflectionTestUtils; +import tools.jackson.databind.ObjectMapper; + +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +class JwtAuthFilterTests { + + private static final String SECRET = "restart-test-signing-key-at-least-32-bytes"; + private final ObjectMapper objectMapper = new ObjectMapper(); + private ActiveSessionRegistry sessions; + + @BeforeEach + void setUp() { + sessions = new ActiveSessionRegistry(); + } + + private JwtUtils newJwtUtils() { + AuthProperties properties = new AuthProperties(); + properties.setJwtSecret(SECRET); + JwtUtils jwt = new JwtUtils(); + ReflectionTestUtils.setField(jwt, "properties", properties); + jwt.init(); + return jwt; + } + + private JwtAuthFilter newFilter() { + JwtAuthFilter filter = new JwtAuthFilter(); + ReflectionTestUtils.setField(filter, "jwtUtils", newJwtUtils()); + ReflectionTestUtils.setField(filter, "activeSessionRegistry", sessions); + ReflectionTestUtils.setField(filter, "objectMapper", objectMapper); + return filter; + } + + private MockHttpServletRequest request(String token) { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/data/load"); + request.setServletPath("/data/load"); + request.addHeader("Authorization", "Bearer " + token); + return request; + } + + @Test + void 重建签名校验器和过滤器后原Token仍然通过校验() throws Exception { + String token = newJwtUtils().generateToken("user", "G3HD", "session-a"); + sessions.activate("G3HD", "user", "session-a"); + sessions = new ActiveSessionRegistry(); + FilterChain chain = mock(FilterChain.class); + var request = request(token); + var response = new MockHttpServletResponse(); + doAnswer(invocation -> { + assertThat(OrgContext.requireOrgId()).isEqualTo("G3HD"); + assertThat(OrgContext.requireUserId()).isEqualTo("user"); + return null; + }).when(chain).doFilter(request, response); + + newFilter().doFilter(request, response, chain); + + verify(chain).doFilter(request, response); + assertThat(response.getContentAsString()).isEmpty(); + assertThat(OrgContext.getOrgId()).isNull(); + assertThat(sessions.isActive("G3HD", "user", "session-a")).isTrue(); + assertThat(sessions.isActive("G3HD", "user", "session-other")).isFalse(); + } + + @Test + void 后登录替换会话后旧Token返回互踢错误() throws Exception { + String token = newJwtUtils().generateToken("user", "G3HD", "session-a"); + sessions.activate("G3HD", "user", "session-b"); + var request = request(token); + var response = new MockHttpServletResponse(); + FilterChain chain = mock(FilterChain.class); + + newFilter().doFilter(request, response, chain); + + assertThat(objectMapper.readTree(response.getContentAsString()).path("code").asInt()).isEqualTo(40101); + verify(chain, never()).doFilter(request, response); + } + + @Test + void 无效签名不能恢复内存会话() throws Exception { + String invalid = Jwts.builder().subject("user").id("session-a").claim("orgid", "G3HD") + .expiration(Date.from(Instant.now().plusSeconds(60))) + .signWith(Keys.hmacShaKeyFor((SECRET + "-wrong").getBytes(StandardCharsets.UTF_8))).compact(); + var response = new MockHttpServletResponse(); + FilterChain chain = mock(FilterChain.class); + newFilter().doFilter(request(invalid), response, chain); + assertThat(objectMapper.readTree(response.getContentAsString()).path("code").asInt()).isEqualTo(401); + verifyNoInteractions(chain); + assertThat(sessions.isActive("G3HD", "user", "valid-session")).isTrue(); + } + + @Test + void 过期Token不因重启恢复会话而延长有效期() throws Exception { + String expired = Jwts.builder().subject("user").id("session-a").claim("orgid", "G3HD") + .expiration(Date.from(Instant.now().minusSeconds(60))) + .signWith(Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8))).compact(); + var response = new MockHttpServletResponse(); + FilterChain chain = mock(FilterChain.class); + newFilter().doFilter(request(expired), response, chain); + assertThat(objectMapper.readTree(response.getContentAsString()).path("code").asInt()).isEqualTo(401); + verifyNoInteractions(chain); + assertThat(sessions.isActive("G3HD", "user", "valid-session")).isTrue(); + } +} diff --git a/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/service/ActiveSessionRegistryTests.java b/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/service/ActiveSessionRegistryTests.java index 5342d937..b4cb9297 100644 --- a/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/service/ActiveSessionRegistryTests.java +++ b/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/service/ActiveSessionRegistryTests.java @@ -2,6 +2,10 @@ package cn.g3soft.fmsapi.service; import org.junit.jupiter.api.Test; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + import static org.assertj.core.api.Assertions.assertThat; class ActiveSessionRegistryTests { @@ -10,12 +14,9 @@ class ActiveSessionRegistryTests { @Test void newerLoginReplacesPreviousSessionForSameAccount() { - assertThat(registry.hasActiveSession("G3HD", "USER_A")).isFalse(); - registry.activate("G3HD", "USER_A", "session-a"); registry.activate("G3HD", "USER_A", "session-b"); - assertThat(registry.hasActiveSession("G3HD", "USER_A")).isTrue(); assertThat(registry.isActive("G3HD", "USER_A", "session-a")).isFalse(); assertThat(registry.isActive("G3HD", "USER_A", "session-b")).isTrue(); } @@ -28,4 +29,46 @@ class ActiveSessionRegistryTests { assertThat(registry.isActive("G3HD", "USER_A", "g3hd-session")).isTrue(); assertThat(registry.isActive("OTHER", "USER_A", "other-session")).isTrue(); } + + @Test + void 重启后首次有效请求恢复会话而新登录仍能顶掉它() { + registry.activate("G3HD", "USER_A", "session-a"); + ActiveSessionRegistry restarted = new ActiveSessionRegistry(); + + assertThat(restarted.isActive("G3HD", "USER_A", "session-a")).isTrue(); + assertThat(restarted.isActive("G3HD", "USER_A", "session-a")).isTrue(); + restarted.activate("G3HD", "USER_A", "session-b"); + assertThat(restarted.isActive("G3HD", "USER_A", "session-a")).isFalse(); + assertThat(restarted.isActive("G3HD", "USER_A", "session-b")).isTrue(); + } + + @Test + void 空会话不能占用重启后的账号() { + assertThat(registry.isActive("G3HD", "USER_A", null)).isFalse(); + assertThat(registry.isActive("G3HD", "USER_A", " ")).isFalse(); + assertThat(registry.isActive("G3HD", "USER_A", "valid-session")).isTrue(); + } + + @Test + void 并发首次恢复只有一个不同会话能成功() throws Exception { + CountDownLatch start = new CountDownLatch(1); + try (var executor = Executors.newFixedThreadPool(2)) { + var first = executor.submit(() -> { + start.await(); + return registry.isActive("G3HD", "USER_A", "session-a"); + }); + var second = executor.submit(() -> { + start.await(); + return registry.isActive("G3HD", "USER_A", "session-b"); + }); + start.countDown(); + boolean firstAccepted = first.get(5, TimeUnit.SECONDS); + boolean secondAccepted = second.get(5, TimeUnit.SECONDS); + assertThat(firstAccepted).isNotEqualTo(secondAccepted); + String winner = firstAccepted ? "session-a" : "session-b"; + String loser = firstAccepted ? "session-b" : "session-a"; + assertThat(registry.isActive("G3HD", "USER_A", winner)).isTrue(); + assertThat(registry.isActive("G3HD", "USER_A", loser)).isFalse(); + } + } } diff --git a/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/service/ExportServiceIntegrationTests.java b/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/service/ExportServiceIntegrationTests.java index 1075a2fa..74cfc1b6 100644 --- a/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/service/ExportServiceIntegrationTests.java +++ b/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/service/ExportServiceIntegrationTests.java @@ -9,6 +9,8 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.util.ReflectionTestUtils; +import cn.g3soft.fmsapi.utils.DbUtils; import java.io.ByteArrayInputStream; import java.math.BigDecimal; @@ -28,6 +30,9 @@ import javax.sql.DataSource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * 导出端到端(连真实库,只读)。验证「元数据 → SQL → POI 流 → 可解析的 xlsx」整条链路, @@ -203,7 +208,7 @@ class ExportServiceIntegrationTests { ); try (Connection connection = dataSource.getConnection(); PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setLong(1, 0); + statement.setLong(1, 1); statement.setLong(2, 3); try (ResultSet resultSet = statement.executeQuery()) { resultSet.next(); @@ -223,6 +228,79 @@ class ExportServiceIntegrationTests { } } + @Test + void 当页区间勾选及各分卷合计与文件内明细一致() throws Exception { + OrgContext.setOrgId("G3HD"); + OrgContext.setUserId(SqlPermissionService.ADMIN_ACCOUNT); + try { + // 只替换内存里的导出配置,真实 SQL Server 查询与工作簿写出照常执行,不改数据库配置。 + ExportSchemaService.ExportSchema schema = new ExportSchemaService.ExportSchema( + MODULE_WITH_DATA, "合计回归", MODULE_WITH_DATA, "b_id", "b_xh ASC", + List.of( + new ExportSchemaService.ExportNode(null, + new ExportSchemaService.ExportColumn("b_name", "名称", 16, ""), null), + new ExportSchemaService.ExportNode(null, + new ExportSchemaService.ExportColumn("b_xh", "序号求和", 16, "sum"), null), + new ExportSchemaService.ExportNode(null, + new ExportSchemaService.ExportColumn("b_id", "记录计数", 16, "count"), null))); + ExportSchemaService schemas = mock(ExportSchemaService.class); + when(schemas.load(MODULE_WITH_DATA)).thenReturn(schema); + when(schemas.filter(any(), any(), any())).thenCallRealMethod(); + ExportService service = new ExportService(); + ReflectionTestUtils.setField(service, "dataSource", dataSource); + ReflectionTestUtils.setField(service, "dbUtils", new DbUtils()); + ReflectionTestUtils.setField(service, "sqlPermissionService", mock(SqlPermissionService.class)); + ReflectionTestUtils.setField(service, "exportSchemaService", schemas); + + List keys = new ArrayList<>(); + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet result = statement.executeQuery( + "SELECT TOP 5 [b_id] FROM [b_con_type] ORDER BY [b_id]")) { + while (result.next()) keys.add(result.getObject(1)); + } + assertThat(keys).hasSize(5); + for (Integer[] range : new Integer[][]{{null, null}, {1, 1}, {3, 4}, {5, 5}, {8, 9}}) { + MockHttpServletResponse response = new MockHttpServletResponse(); + service.export(new ExportService.ExportRequest(MODULE_WITH_DATA, "b_id IS NOT NULL", + null, keys, null, null, range[0], range[1], null, true), response); + int expectedRows = range[0] == null ? 5 : Math.max(0, Math.min(5, range[1]) - range[0] + 1); + assertSummaryMatchesDetails(response.getContentAsByteArray(), expectedRows); + } + + MockHttpServletResponse response = new MockHttpServletResponse(); + service.export(new ExportService.ExportRequest(MODULE_WITH_DATA, null, null, keys, + null, null, null, null, 2, true), response); + try (ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(response.getContentAsByteArray()))) { + for (int expectedRows : List.of(2, 2, 1)) { + assertThat(zip.getNextEntry()).isNotNull(); + assertSummaryMatchesDetails(zip.readAllBytes(), expectedRows); + } + assertThat(zip.getNextEntry()).isNull(); + } + } finally { + OrgContext.clear(); + } + } + + private void assertSummaryMatchesDetails(byte[] bytes, int expectedRows) throws Exception { + try (Workbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) { + Sheet sheet = workbook.getSheetAt(0); + assertThat(sheet.getLastRowNum()).isEqualTo(expectedRows + 1); + double sum = 0; + for (int row = 1; row <= expectedRows; row++) { + var cell = sheet.getRow(row).getCell(1); + if (cell != null && cell.getCellType() == org.apache.poi.ss.usermodel.CellType.NUMERIC) { + sum += cell.getNumericCellValue(); + } + } + var summary = sheet.getRow(expectedRows + 1); + assertThat(summary.getCell(0).getStringCellValue()).isEqualTo("合计"); + assertThat(summary.getCell(1).getNumericCellValue()).isEqualTo(sum); + assertThat(summary.getCell(2).getNumericCellValue()).isEqualTo(expectedRows); + } + } + @Test void 总合计接口在没有配合计列时返回空() { OrgContext.setOrgId("G3HD"); diff --git a/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/service/ExportServiceTests.java b/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/service/ExportServiceTests.java index c9d7bd00..e6592285 100644 --- a/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/service/ExportServiceTests.java +++ b/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/service/ExportServiceTests.java @@ -113,9 +113,33 @@ class ExportServiceTests { @Test void 排序缺省时回退模块默认排序且加引号() { ExportSchema schema = schema("b_xh DESC", List.of(leaf("b_name"))); - assertThat(service.resolveOrderSql(schema, null)).isEqualTo("[b_xh] DESC"); - assertThat(service.resolveOrderSql(schema, "b_name ASC")).isEqualTo("[b_name] ASC"); - assertThat(service.resolveOrderSql(schema("", List.of(leaf("b_name"))), null)).isNull(); + assertThat(service.resolveOrderSql(schema, null)).isEqualTo("[b_xh] DESC, [b_id] ASC"); + assertThat(service.resolveOrderSql(schema, "b_name ASC")).isEqualTo("[b_name] ASC, [b_id] ASC"); + assertThat(service.resolveOrderSql(schema("", List.of(leaf("b_name"))), null)).isEqualTo("[b_id] ASC"); + } + + @Test + void 不完整或逆序区间不能退化成全部导出() { + ExportSchema schema = schema(null, List.of(leaf("b_name"))); + for (ExportService.ExportRequest request : List.of( + request(null, null, null, null, 3, null), + request(null, null, null, null, null, 4), + request(null, null, null, null, 4, 3))) { + assertThatThrownBy(() -> service.buildSelectSql(schema, schema.columns(), null, null, request)) + .isInstanceOf(BusinessException.class).hasMessageContaining("区间"); + } + } + + @Test + void 导出列采用选择顺序且跨组拖拽后保留各列分组和权限() { + ExportSchema schema = schema(null, List.of( + new ExportNode("A", null, List.of(leaf("b_a"), leaf("b_b"))), + leaf("b_c"), leaf("b_denied"))); + ExportSchema result = new ExportSchemaService().filter(schema, java.util.Set.of("b_denied"), + List.of("b_b", "b_c", "b_a", "b_denied", "b_unknown", "b_b")); + assertThat(result.columns()).extracting(ExportColumn::field).containsExactly("b_b", "b_c", "b_a"); + assertThat(ExportHeadBuilder.headOf(result.nodes())).containsExactly( + List.of("A", "b_b"), List.of("b_c"), List.of("A", "b_a")); } /* ================= WHERE 拼装:数据范围只收窄不放开 ================= */ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/JwtAuthFilter.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/JwtAuthFilter.class index dfc53f4b1e95b77d01f3038f02f2bf5ec2f33ac7..04b4d42c63e8a7a18f4b17aacf51fd65a9b979ee 100644 GIT binary patch delta 1448 zcmZXU*>hAy6vlsdwmVBFBvXMHLKSNykW3{dpoozW5i}S`BpD(K!Z349CJZxS=Awd2 z)F|%jb;S)gz}*NAnV^6hMqE%I{SSQc!8fZY%hNZh6s0xQ_jG^V-|2I{b9(-szkh!C z>_1~Cfi`{{2`}@_ZRHxpwUir#3Ra4H=7kV((Cf=c)`lIry^`&tI5rZZEQEFbJ7|TNj-z;%=L}SC z$AFcz*9+as8`O2!xm>2uG~jFt&JGUqg5pJ9a`p%7BQNuc$*YRjc>QQ8*yJ_7aQ=wQ zHcmMqRcCzVG|Bgyqj9y=unzJD25(BeJq$3Dw=P&$3wALJW&ds24~TuoJ=emO|HCA} z|6qPu`(4%{=g+bk3lC5FfGUp2T**Fi;$7Yo>*IYskoyif4dqL==(FW`<-34N>X<85 zb7sg1m^aDMvk&=5{Cq5E0PigCDuYjC?l)vZJ00OuadQZal&nm_GR|0e^YR*Bk+ntI z4^XzaG1?jJDst(F23L-UQ4)uVtO$s8+NM?4#Mvr6iVVxOUUa;h znC|SJRy))}z33BTrSwcI%V^;Wc}rKbmNm4?ds#=E4)L&o?Q{x%gS2udJLz&W+(=Nu zjBr%y;3q1pXdUCrwTZ9uU%(eC^6s=5=Ko6?jaBM_kO8ugE8@?v)?J8ltHZw|H3Jcm~xN9Vn6! z5l_&(Ihs4p?o&f9Rw!~?kvnuPCe(>a4$w`A9;srSIrO^u&YAE!l~HUmqmWemThYMd z);5IaJTpuxLKl|T!*VVczK-vRSglR-(5%=fcdzg@R5D)TJ`WAihe|x6?c*h$^sx9C z&oq>H-ayy+z{pgHGeryJ56P6mRFL2zc1V~>nn<~!E}ghwD%56Ia^1wuoZ&kORnJo6 z617*HEaH34O4dIJtb2U|`B9M5g7^fP5?u>^M#i`dFF(oG!_QNX?wqWDkr5V3tGNA@ GntuU_S{N_@ delta 1514 zcmZXU`&U#|6vsbzUUwds2}ct_b0K6ZkDQf{)GQcz_43HLe+BGMgLXrHy8Eb7W~G9*?%QYD}^AyC&yNTj{5S z#RgRbHGJ{(YDJTuavnB#ge6vwt4gb}{&LL<)UsS>g~3YdtXy~Hq*_)PJW8D%Xx79d zv5KYuYiQ6}YtTrOwaXnVuH`X<$MI;m8(Sg`F)vY#jM7bJb6YIBwZW8Z0{B^P(830* z$a7~^n}&X`?2N>6{baJyAjTLCf81<{nep1F+3dyC2wgdK;&NZlm3=2KpWbu1>&Vrk zJN$&%Y~ZF&1wLu8g{@Y=iglT>=mtM`@T|deJa75*MZqp! z(0S2dC*2xTt#iISYr8&v^e)N0rn;ZM4gnzAmn6 z>EbnmJxZ2fJYp_vYc(USX7!f#h>x3j!(cD_tn2z*Zx?SGyv5sAiFaynKkw?iXYf8B zXyjX?y!prWdVNmUIgK1^Vj#z<^ObczFj>1`U6t|Tv7j+iOMJ*jXnZUu@1T_&4&2~p z33eM(Wc{G5d&GWX@0-uv|HCAu|6p$M-FDO=iJC5+Ce~drfYxu*d@5tUVudjrmeEM> z;0T|I=i(@RlEESC?~EDsYI6{$jJGn1$xITfV#dl2$QzDPvwl7oo@0V~a87V8&^Ru0 zwD;=-a7tDOuIIY3~ZM{Ks|Y`^e9NRH_CVH?I|8y(_xNP{TrL>01@aaQ1_@rk>b z&3qQfr7Pt=xi5>Tq)e_%Io0B?q(ZJh6-|P)utd13g>4xLYK3c=bV;qW*>YQkC3vNl z00VM-ZplIAmXdt31Utb=!KBU+k-F0&C2a5%r)>?pIm1~A%p*D+tTY^rr$*=5axaa~#QK&UZ zdqPA$Nq9vlKES54Hugl4r;;T4BqA+U$QHE@AN6w9tH@=wU2txib)=-?;dA#%UwkJb zIP4Da;kd?FolM`2P2gY-b0t2Z5 zoYxCe^lGR!ct>?pI>j8(0(qZwn&rueGLChU=6Y%Q7CY6MY5tK^!zjt6?KjF>GsO3j ztNflzlC@Jvrtt$mO4UCJs%}IY`B@MpyGxJ}(Usss@?Db8$uBZG_;uv6{wDtK^7$pC JL}>pY`(FP*`+lGOzMtp&9w)}O zmb#Dqv;TDf75J;&yZLaFYkCu|~k9$L8vd zVXcnZqA{~fc|_Ifg4XC{H*~D^z=2zgJ+2{_Es*`&JV;`lKu%cG7pRe@(Dc=6tuByN zG^W~(J8-9hyF3_;^+uNxNGiBTU|1$`MPpd6kB0Pc?ex{Pp|~E7X$m$9jL1l4H$|6) z8mnTB1n(!hSD>U;3)Yn-Vk`9EifBTOhl8QhNrIJ4iLe$*B+`HQS&hvF9^8)y1cpS_ zhS1cQR;%g@3Ba#}u4;Nh;F2O^m?Phq<8>HI?Mkj2o3J(;m*OE08etfJE5#Kp0(lv{ z%2+HCQZ+Z4@rVbl*ec*zW{sKUFe|axjmNM}!Q-+LPZ*0`SGbz7-Gd!?s$;wBh!A@_ z!tR=^q5)|G0vGkG|Fun;9*(lKsTvuFa;6k0_())AhU9FKz!GDNx7rxzaCXe`mWiZ~ zta?IkRO45L^!nKHgbUxZ7GmUy-t{MU^z^mv>D#ukuVq7D%ig}0wp=HEbmJ%dtl$?} zwqFHCXC@14t6~kI)W)zXS;cjaHF6uZ7WG3<2|1&WQ_fVi<&jWg^sHE{p~)z>4>E$z z?7WPIl6h<5A)~@RP}CSVx`z8wv8ie#GB>P;CL8N=M{pbcE>L(5jb-bMIpPZb5E%a7 zQURw46vrsIcpd=3cQ!n1pJva=_8GP=whs2n1$MFoqTNQGA(#1=;pJp1|os|=)r}|ZsW*nd=4yzye(KL^pbX` z(d(ON@_9|3Af1oHNR;+tEKD;NrWjoqhJ&QF7zdlqWK1$zv38D0Mu#vNExtqN;d_I8 z`SdC4o8}XkkfPh1H_D{5Dm}%dvgY%pseCD_OugiYWcgnNZ<63_02^}QV?pHF8v*}( zlVLnp11N{WyEX@v7>KF30Ml5>>0H+;6k`U)Vb%iY!keNmtjtGaM=CvNS= z?QN6nJ=nCUWIyf$x9Eet%#??VQGmNs?FJf>M`W;>vWB=z7`dK`8+cfjn&u`jR*0k* z$MCM{zbx&uEaj6lIF9$2J{v`NA0Lo0vnQlVI+i{?u$@Al0__?FAND^>#~HNt>c>o@ zVp+=W0rsTbWNGnXC}J1f#U=Z&#Rk)~eRtpxnztf1a1hOt?D_UiJiI&5jYqrjq&%UP zyP>=efEC|HykW@IXM#pAs(XiEv0&Fu2TtVG~vDo3oT13sU?!br^ z1w&SaH-w8D!jYB5RgH}eb>WDPX6*7}H(Jd#?xCK&*r#E?2ywtP-IcCp9Q5K44)=U< ze^QjoJy$)8lFY)?@}8E|R>gE1c~Ojl3n~#6em(F~9-)>@d6d#c>6BVKwPI=trPKce zN;d~8j`1s;foz!kiU4%Lah#x3a1y6z7mJr7i4WlOKZ52D#E?=HgZabIq9TSaAx0ww z`7+K(%h!&>RXB|^GPVzAagN^Sxyss2*e=j^kuwg;OSr<3Tjez93|!@_;+o9NM~CZt zI~GA%ha2cbE~BfQxykodEpF3U!EoGR7VYN6^fJlklRQBdWIRT)5F%HLaZG}7jLqmm z9=b?tF=olkgiSD7whNpSj7~)|T6}l^!FP{*Im}bkJ;A4-)TV357$xbf4pc}gYrnJv zRhmuJUoSBuSpJLPJ_-H?P?3r>7DTkw^k&YL3?=LaPzena*u{xR=c&&|6)QQJ=Wz-G zn2HiiLpiExoz79^0N&~ZZ}lKvG4oH}2P|U^W*~_u=GV-8$zkve0JF$3n+JLhS>_Jt zmXX-Uu=mmE)s2ULbW7O8j+U*5wnvP~%kUj38tMmE?lE2FBf7vYN*kr1$PE-3kvLZO zAu==g5t`BoJs3k%9Gf1{?2y;TI{FKVQ7b|H*3txMt_{j?h~Wu$^04}GJmn_+O%+~I z!7NC)>=di$0Y>eZhyVZp diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ActiveSessionRegistry.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ActiveSessionRegistry.class index ffaa78c51293586ee1de4d1cbb657a2473b470ec..e2690636580474af97b4a7a81952dc8cc31b11d5 100644 GIT binary patch delta 495 zcmZvY%PvDv6o$Xkvk!54(V(OkgC^?II_|eqN;@MV1~oA(sYFOMX_<;w(2*yQU_eL( zi5PXj6L=R9>l{r@4EA1s_WIXa-`Zz^PoMez{rn2#$laMAS~#NNEgx6c_HsLwVyS8p zBjIC^p<1UWe!uFu*7^&()%}xVzF4X3AC)xx`D$VJV53mpD(oBojIFhw>WAOCPAE1X$*PoP12sQnVnq=tDD4ki+eq9Vm3VCz}%rWl_5PcC^pP&> ziMY-iv9t=S20kmS>I_`at}apfja2J6m|ZSbuPoJvp9l$}F7C|lgch#d!nIqtb`v)t xOD+hw!j_JHHVv=#MWUUUp4I+aMv)eyheehoI^Ud;xGYDB4zeQ3bd{GZksmg4I==t_ delta 596 zcmZva$x6de6o&tsq(YN)pt0hxu@KvINS)`gP8)F{b>lLYphang6vd6jN06-a6)b`% z6%|~%&?o2%xbzXkbJG$xE|T+e&!2O?drn&FWmx|Bd3gg+z~;UD$rPPIJg{pFNy9y3 z>8_-T6b$#9C3+R~AfqA+gFz`(D>bWJaklL9hlp^?^kU5_9c^1DyVm}(%@94boI>N)dqzS)6GII$ir_?Hia?C`HH4@azeSr$ z_o;NBO82Ss4%GtrB?t=ppKlRM9lrVwO^^4|e55u}S^&opJRCk=pwGh?Y} o7FaZ^mj9EfyZIH~EM7 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 LvW@=6K!pAW4j5H6 delta 5826 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 ZB}5QKoOgyvCP?s|eU$mpkwn_7d;_lnN$&sv delta 492 zcmZ8cO-lk%6g~HiI-^ZCsg;?j)ntBT7NoV1AllTXs32$&G|5nkC^%NDsGpF{+JDfd zMF<78Ytet`5A-9VJ7OR`xc8j*IQQK1zLal8`}}!%1F(r@8G?|qJCuy+D>7UJt!i8u z`I=Eb&+na8&2meIhp@(yxSf^!KK-C^eb~5cmQDMfin6CvsaIMX4^)VFK^RscL-9ev zhzc(>f|N=h*#$bWKd33mm~NMJinooB62*)onQh;M?R8P41-l`#QW_b%Bld&pPQ7k6 zw`zuEnU;(kA=i%(xg{vOrq$YRSS=)Ysl??5X_~W}|0QvG;e)^svsFgH=*wiEp=P@9 zKk-dql)Hx~Ax;rst~#8jf(S8=ak&ZdoyO1OnBeZ1Ei&?c9+DoNKHyRwa1nhMLKu^* ziuANo{X9-y-t>y-aUj-3;{Bh~#emaa9v9|%DjoAFCN3nALY7&8?HxPkxH;dFM?s#s GK=K!HojrK~ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService$ExportRequest.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService$ExportRequest.class index 7f568c977285ef8919b48c9802889ebd8f73c4ee..d679dcc8e86c61292d4cc8f34f544b74746a5c96 100644 GIT binary patch literal 3542 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 literal 3140 zcmb_eYg5!#7=BI!0!sv01yQM53kr+6Qf;lS(kh~27gW|l)!xDqA+npSNfyE0XFAhQ zoqlSkKfsRt;3&>`104IkKciC||3qt_lVl-Ws?OLC$;ml+ug`lsfBZA^7{C#Ht3nYt zkhPK{dtLjIm%NmB^@5pn4QJfU8p-a7g6(*jVAtGlxWycIUcY@cV;7yQp`uJ+&1HRD zPv&%MB-w9dZD&|Tg~0w72pzPYu_4!#Sr9(vV-drE*6Ju{a~o31BCWy}#v_lgb;-oA|BPz0yb zLzj)Lm+DNZ*dwskkCQLyh~wy!Qk+bcH#Jlt7DE#IByw*tC0cvh24wE+*YFMwP$i=n z2$N{`M1c==#E`;a6^Cfj%fY0f6YmPtShkbbbLKT;K+hF@kCr>Ig1%~&ahHmtl(68I z^hm=obh8}v;o&8b4=ruG5U10dN#fX7S;m37D8S{WHEc`>G%U`B$wDb3nwRjbh&lO7 z!C6|ztX0--rDq2v5Ql;@8v1Zn;B@ONq*)ZFu)3&*ErOLaY)CcdH1y*GfzDS_L*pxq zD@8r$N|_&O7{GafGp{0Zg`l&YHHMYD7(gax46E>w%+`ejn$b#*isnhJImEWjLYup|)k^!PYcRH?T zJ=>X-ZR?W2FVO}QQU?N0>eg`1aGTR?`Ncvi;$;}0$jKtcbIi6C$S+3dOZgl*3Pp73 zxmFwFMAAf%G)6|n6Zp~f4^Yreo2e*nC#*kN$6WFrg zv%?W%=8a^aBt%Dp1)JS~11vyI;LiW)x^()O;btAPKy;(-cja`~jUI&XMe$gKUjTe6 zS`i^$#N=`%95#pfft!lV)mnBGJ{@F!(a8cxh2d7f5u zJiy*`+2I@5*e-2qn#S7-9^&9M-cvC6og$>E9oL`zbp!Ri($t;4smB9T+oz~RBLI7; zuFN-fGXa2dADZ$Ib;>kaxb9JAu}^t~ZsoCWe1fzzAv9j&d^>@YlvIOl=pk$h_Js~9 zI7LbcUqJOfzod(sQ1&!w6 zzBMu+%pLUdy_BWn=6|x(ajG)s=7Jh-W(xoI<8TH!416)kB-$PBJ=^i@n@F;>uMCB*^Q>DmI z5*VU~PkqwQpmW_wStBGK4c`TZQ{W0*cqoSdd8xxVDa&yclNFV?hA;3HHsEVtP72>} Mt)cRuTKro17w?rUfdBvi diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService.class index fbc3f3d656dbb01cd0789edfe58bc3376166faf6..2cdb18e862bf2879cb51b09d7afb61e8b5c94808 100644 GIT binary patch literal 28716 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 literal 28738 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 delta 1561 zcmY+E`EOKJ6vsbz=FPn6%xg=5Vy3o0NJv_#QYf@^p)f#MEXdYUzy-$A!az&0ow8JA zRB%B=&?mCEf=g9ES)>an=nooWjERvLqe=bM_$Q#De%}-nX1V9wd(ZvebI$ks-l?MA z!tnPuE?og)Ts;@=^M^(;#bGL?Zk5%XHJvh>a)*$E@TAb)i+6tK&m^!Y48ixS03|8TyqE!)L&#{^7FppY8xmyx0cfSdY48>UB zu#iRWAAzxvIu_e3ad?2GZYVfqVjasJRSg4##XGjtwfBY;|ZSXb83>wkA3^MX2L3hYmX3A$z>(a>s|FX2*rf zkYibI8b-S%X0+etZ8z@ZTJIW4lwW~+C}WfsPr7HEiROa~KRaDMtKVzGq0CY@l^wJ$ z8HzP2b}wdUTVEO~G^t1_)!Log+B=eS+grQZwo;;8ed5WG$4++1c5CM2ar_zs1hw*n zR!U`!Y~Y?g%(-;PlN!V6!}(Yguv>FyIEWNaX%K^28mOV=}XYL>j1#yO&Q zt`s(p4b-xc`6Nj#P`eg#fF&GZDIc<&^OEIv)a$ZU{3Lr#_IDc7ZS7WLt2xRu>>)#t zhuJHx!UEpMvph#csSNTwFUWe{K3+@{G01-Hdi)OZl03Cv-T_`ti51U1s6~(DU&)sj zR3$%hh*z+c|0!O@6^E}Wa+;!sFFSnmvi-dNAMlZzzz3D$Ewp1oJFb)USVKDM*t}u$ zmQ7vymx|r;GHn`c-Z7fP-^411bV0_Q(RT!+&=b5TXiGJ|uiim4sNzxCs9;noDiT!( zJ^K6jNX^pos63@bQ$)(I^q|SpPoBiL#9!ktvvRGxlPt@%CZ)A@lAKAolH@iYUyOR@ud#t~_1#P*8B3oZ^w&hv!yJ50PSXQ?!=p89G~hR@V$GW~xYpbs}&*sR(gK z>)la8lSH&ZLTQ#bTJ)aU$Vw7=>BRMIO|xI4TuYe5$9$spW@-PF*!Ku*;)q{;H4j;d=JUFop1TJ$)|$G$<97%*n+Y@6gijMSKO9s9a2 w^ctMu(@nQG^qg?dYsJR}K9}{lh;dO~o_1n-5PE|l`t$%_)`Rke>=mN_0{5yWH2?qr diff --git a/code/fms/fms-api/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst b/code/fms/fms-api/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst index 544190ff..f4627835 100644 --- a/code/fms/fms-api/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst +++ b/code/fms/fms-api/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst @@ -1,3 +1,4 @@ +cn\g3soft\fmsapi\config\JwtAuthFilterTests.class cn\g3soft\fmsapi\service\AuthServiceTests.class cn\g3soft\fmsapi\utils\JwtUtilsTests.class cn\g3soft\fmsapi\database\OrgDataSourceManagerTests$1.class diff --git a/code/fms/fms-api/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst b/code/fms/fms-api/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst index b55b9e67..509776cd 100644 --- a/code/fms/fms-api/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst +++ b/code/fms/fms-api/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst @@ -1,3 +1,4 @@ +D:\workspace\code\fms\fms-api\src\test\java\cn\g3soft\fmsapi\config\JwtAuthFilterTests.java D:\workspace\code\fms\fms-api\src\test\java\cn\g3soft\fmsapi\database\OrgDatabaseConfigLoaderTests.java D:\workspace\code\fms\fms-api\src\test\java\cn\g3soft\fmsapi\database\OrgDatabaseConfigWatcherTests.java D:\workspace\code\fms\fms-api\src\test\java\cn\g3soft\fmsapi\database\OrgDataSourceFactoryTests.java diff --git a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.config.JwtAuthFilterTests.xml b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.config.JwtAuthFilterTests.xml new file mode 100644 index 00000000..586fd182 --- /dev/null +++ b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.config.JwtAuthFilterTests.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.ActiveSessionRegistryTests.xml b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.ActiveSessionRegistryTests.xml index e78d1de8..5a3b0006 100644 --- a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.ActiveSessionRegistryTests.xml +++ b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.ActiveSessionRegistryTests.xml @@ -1,70 +1,67 @@ - + - + - + - + - - - + + + - + - - + + - - - + - - + + - - - - + + + - - - + - - + + - - - + + + + + \ No newline at end of file diff --git a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.AuthServiceTests.xml b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.AuthServiceTests.xml index c292dc27..b45cae41 100644 --- a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.AuthServiceTests.xml +++ b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.AuthServiceTests.xml @@ -1,69 +1,63 @@ - + - + - + - + - - - + + + - + - - + + - - - + - - + + - - - - + + + - - - + - - + + - - + \ No newline at end of file diff --git a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.ExportServiceIntegrationTests.xml b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.ExportServiceIntegrationTests.xml index 72589286..06108b84 100644 --- a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.ExportServiceIntegrationTests.xml +++ b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.ExportServiceIntegrationTests.xml @@ -1,31 +1,31 @@ - + - + - + - + - - + + - + - + - - + + @@ -34,44 +34,43 @@ - + - - + + - - - - + + + - + - + - - + + - - + - - - - - - + + + + + + + \ No newline at end of file diff --git a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.ExportServiceTests.xml b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.ExportServiceTests.xml index d9d6d427..3c9823cd 100644 --- a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.ExportServiceTests.xml +++ b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.ExportServiceTests.xml @@ -1,31 +1,31 @@ - + - + - + - + - - + + - + - + - - + + @@ -34,53 +34,54 @@ - + - - + + - - - - + + + - + - + - - + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.ExportWorkbookWriterTests.xml b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.ExportWorkbookWriterTests.xml index 349c35f3..7ec0b47a 100644 --- a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.ExportWorkbookWriterTests.xml +++ b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.ExportWorkbookWriterTests.xml @@ -1,30 +1,31 @@ - + - + - + - + - - + + + - + - - + + @@ -33,42 +34,41 @@ - + - - + + - - - - + + + - + - + - - + + - - - - - - + + + + + + \ No newline at end of file diff --git a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.utils.JwtUtilsTests.xml b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.utils.JwtUtilsTests.xml index 8f5e7b7f..6ab790fe 100644 --- a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.utils.JwtUtilsTests.xml +++ b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.utils.JwtUtilsTests.xml @@ -1,69 +1,63 @@ - + - + - + - + - - - + + + - + - - + + - - - + - - + + - - - - + + + - - - + - - + + - - + \ No newline at end of file diff --git a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.config.JwtAuthFilterTests.txt b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.config.JwtAuthFilterTests.txt new file mode 100644 index 00000000..69d4db04 --- /dev/null +++ b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.config.JwtAuthFilterTests.txt @@ -0,0 +1,4 @@ +------------------------------------------------------------------------------- +Test set: cn.g3soft.fmsapi.config.JwtAuthFilterTests +------------------------------------------------------------------------------- +Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 10.25 s -- in cn.g3soft.fmsapi.config.JwtAuthFilterTests diff --git a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ActiveSessionRegistryTests.txt b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ActiveSessionRegistryTests.txt index 39d18265..eccc98b4 100644 --- a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ActiveSessionRegistryTests.txt +++ b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ActiveSessionRegistryTests.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: cn.g3soft.fmsapi.service.ActiveSessionRegistryTests ------------------------------------------------------------------------------- -Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 s -- in cn.g3soft.fmsapi.service.ActiveSessionRegistryTests +Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.183 s -- in cn.g3soft.fmsapi.service.ActiveSessionRegistryTests diff --git a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.AuthServiceTests.txt b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.AuthServiceTests.txt index b22c6746..381e1a7f 100644 --- a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.AuthServiceTests.txt +++ b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.AuthServiceTests.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: cn.g3soft.fmsapi.service.AuthServiceTests ------------------------------------------------------------------------------- -Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.218 s -- in cn.g3soft.fmsapi.service.AuthServiceTests +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.993 s -- in cn.g3soft.fmsapi.service.AuthServiceTests diff --git a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ExportServiceIntegrationTests.txt b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ExportServiceIntegrationTests.txt index c4a192c3..565a0704 100644 --- a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ExportServiceIntegrationTests.txt +++ b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ExportServiceIntegrationTests.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: cn.g3soft.fmsapi.service.ExportServiceIntegrationTests ------------------------------------------------------------------------------- -Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.740 s -- in cn.g3soft.fmsapi.service.ExportServiceIntegrationTests +Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 29.90 s -- in cn.g3soft.fmsapi.service.ExportServiceIntegrationTests diff --git a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ExportServiceTests.txt b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ExportServiceTests.txt index 422b2b35..b351b432 100644 --- a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ExportServiceTests.txt +++ b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ExportServiceTests.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: cn.g3soft.fmsapi.service.ExportServiceTests ------------------------------------------------------------------------------- -Tests run: 17, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.171 s -- in cn.g3soft.fmsapi.service.ExportServiceTests +Tests run: 19, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.294 s -- in cn.g3soft.fmsapi.service.ExportServiceTests diff --git a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ExportWorkbookWriterTests.txt b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ExportWorkbookWriterTests.txt index 22f9eac4..08b7085f 100644 --- a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ExportWorkbookWriterTests.txt +++ b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ExportWorkbookWriterTests.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: cn.g3soft.fmsapi.service.ExportWorkbookWriterTests ------------------------------------------------------------------------------- -Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.263 s -- in cn.g3soft.fmsapi.service.ExportWorkbookWriterTests +Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.362 s -- in cn.g3soft.fmsapi.service.ExportWorkbookWriterTests diff --git a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.utils.JwtUtilsTests.txt b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.utils.JwtUtilsTests.txt index 630bc087..a0f5b067 100644 --- a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.utils.JwtUtilsTests.txt +++ b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.utils.JwtUtilsTests.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: cn.g3soft.fmsapi.utils.JwtUtilsTests ------------------------------------------------------------------------------- -Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.099 s -- in cn.g3soft.fmsapi.utils.JwtUtilsTests +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.089 s -- in cn.g3soft.fmsapi.utils.JwtUtilsTests diff --git a/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/config/JwtAuthFilterTests.class b/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/config/JwtAuthFilterTests.class new file mode 100644 index 0000000000000000000000000000000000000000..3818e35d0f7fbf16feffa3a5ecbb25e6f992d282 GIT binary patch literal 8112 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 X#UF9b8-2pj7hDUFg}>rUjG*ZMQoTjG delta 668 zcmZva-A)rx6otPtKs$`5&suhj5LOvoSeP(UbD|y`^;JHLOVZy|F{4aSo-Ar$vVX8(WK~A zB&%Eg!p?E+r2Mw*`?Z68#b{7eId^>1p^yGJcQgYGDl)-)D;4}wuRAhi;|ytrxvTIb z?FEO{Y@ba|(e7WhDQuZrMm1xM2gB->H_=qvcF0iB+&2wkHGlo+UD?o!ng=`#KB$$V z&9p*SrmDMzu%pdmMcbS8)#Wvtr;5bEakYFDRlj|X^{U{bQ%IBL7M8eGxOC7dQH$(0 zVv?B&&h=3JnPBIV6Q`ASW(8}hLXzjakQ3&FZ!sg$7vY#%#ruxqCSA8d_c^y4q&K3B zd#}OBKj1Zih?{TX)*DRz-$1^(%H=n{%3T&lV_Iec3GvY>151*ZXpSz{=w^?eCf$`# z_e_l`WAh8{{rWZjr>ry`Y)1U!jASc$Y;dI||q?pj|;ri%O}3HoIX$a@a5 SD5|gLS-L@&vV~+Umi_|94rm$x diff --git a/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/service/ExportServiceIntegrationTests.class b/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/service/ExportServiceIntegrationTests.class index f6314188812c14de14272dbb7c7c185c6da24c9b..b2ce9c11508eac7542befa25e966020e97420f1a 100644 GIT binary patch literal 18831 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 bsfX45FngQvd;rgFc($VcLG`eDgtGq^XEu6h literal 13872 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 BO@jac diff --git a/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/service/ExportServiceTests.class b/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/service/ExportServiceTests.class index 8f8dd2321854edd94dd7d5116d3544a01820aeee..47e20269ab023d7c13542b3655a9eb696cbd8581 100644 GIT binary patch literal 15663 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 TAMlU(CuxKDr~GsNC5`?Umda|9dm@Ci6lN3o`TG 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 diff --git a/code/fms/fms-vue/src/components/I18nNameModal.vue b/code/fms/fms-vue/src/components/I18nNameModal.vue index 361009bc..f2dcadcf 100644 --- a/code/fms/fms-vue/src/components/I18nNameModal.vue +++ b/code/fms/fms-vue/src/components/I18nNameModal.vue @@ -93,7 +93,7 @@ function onCancel() { .i18n-name-row__label { flex-shrink: 0; width: 80px; - font-size: 13px; + font-size: var(--fms-font-size); color: var(--fms-text-secondary); } diff --git a/code/fms/fms-vue/src/components/captcha/SliderCaptcha.vue b/code/fms/fms-vue/src/components/captcha/SliderCaptcha.vue index 36930ee6..e494173d 100644 --- a/code/fms/fms-vue/src/components/captcha/SliderCaptcha.vue +++ b/code/fms/fms-vue/src/components/captcha/SliderCaptcha.vue @@ -277,7 +277,7 @@ defineExpose({ resume }) justify-content: center; width: 100%; height: 100%; - font-size: 13px; + font-size: var(--fms-font-size); color: var(--fms-text); pointer-events: none; user-select: none; diff --git a/code/fms/fms-vue/src/components/fms-file/FileCard.vue b/code/fms/fms-vue/src/components/fms-file/FileCard.vue index 492b7713..40b2484a 100644 --- a/code/fms/fms-vue/src/components/fms-file/FileCard.vue +++ b/code/fms/fms-vue/src/components/fms-file/FileCard.vue @@ -150,7 +150,7 @@ function onMoreClick(info) { width: 46px; height: 46px; border-radius: 6px; - font-size: 13px; + font-size: var(--fms-font-size); font-weight: 600; letter-spacing: 0.5px; color: var(--c); @@ -245,7 +245,7 @@ function onMoreClick(info) { .fcard__title { flex: 1; min-width: 0; - font-size: 13px; + font-size: var(--fms-font-size); font-weight: 500; color: var(--fms-text); line-height: 1.5; diff --git a/code/fms/fms-vue/src/components/fms-file/FileListPanel.vue b/code/fms/fms-vue/src/components/fms-file/FileListPanel.vue index 96dea228..007bcc6d 100644 --- a/code/fms/fms-vue/src/components/fms-file/FileListPanel.vue +++ b/code/fms/fms-vue/src/components/fms-file/FileListPanel.vue @@ -740,7 +740,7 @@ defineExpose({ files, triggerSelect }) .fl-table { width: 100%; border-collapse: collapse; - font-size: 13px; + font-size: var(--fms-font-size); th, td { @@ -865,7 +865,7 @@ defineExpose({ files, triggerSelect }) justify-content: center; gap: 8px; min-height: 160px; - font-size: 13px; + font-size: var(--fms-font-size); color: var(--fms-text-secondary); &--fill { @@ -948,7 +948,7 @@ defineExpose({ files, triggerSelect }) /* ── 删除确认弹窗 ── */ .fl-confirm-content { - font-size: 13px; + font-size: var(--fms-font-size); line-height: 20px; color: var(--fms-text); word-break: break-all; diff --git a/code/fms/fms-vue/src/components/fms-file/FilePreviewPanel.vue b/code/fms/fms-vue/src/components/fms-file/FilePreviewPanel.vue index 6e05d710..07367b26 100644 --- a/code/fms/fms-vue/src/components/fms-file/FilePreviewPanel.vue +++ b/code/fms/fms-vue/src/components/fms-file/FilePreviewPanel.vue @@ -148,7 +148,7 @@ const extUpper = computed(() => (props.file?.mx_fileext || '?').toUpperCase()) &__name { flex: 1; - font-size: 13px; + font-size: var(--fms-font-size); font-weight: 500; color: var(--fms-text); overflow: hidden; @@ -210,7 +210,7 @@ const extUpper = computed(() => (props.file?.mx_fileext || '?').toUpperCase()) } &__name { - font-size: 13px; + font-size: var(--fms-font-size); font-weight: 600; color: var(--fms-text); word-break: break-all; @@ -239,7 +239,7 @@ const extUpper = computed(() => (props.file?.mx_fileext || '?').toUpperCase()) color: var(--fms-border); &__text { - font-size: 13px; + font-size: var(--fms-font-size); color: var(--fms-text-secondary); } } diff --git a/code/fms/fms-vue/src/components/fms-file/FmsFile.vue b/code/fms/fms-vue/src/components/fms-file/FmsFile.vue index d47204e0..5fd87517 100644 --- a/code/fms/fms-vue/src/components/fms-file/FmsFile.vue +++ b/code/fms/fms-vue/src/components/fms-file/FmsFile.vue @@ -132,7 +132,6 @@ defineExpose({ moduleId, refreshCategories: loadCategories })
- 正在解析模块...
@@ -246,7 +245,7 @@ defineExpose({ moduleId, refreshCategories: loadCategories }) justify-content: center; height: 100%; gap: 8px; - font-size: 13px; + font-size: var(--fms-font-size); color: var(--fms-text-secondary); &--error { diff --git a/code/fms/fms-vue/src/components/fms-module-common/schemaRender.js b/code/fms/fms-vue/src/components/fms-module-common/schemaRender.js index c17731d1..5a32a90a 100644 --- a/code/fms/fms-vue/src/components/fms-module-common/schemaRender.js +++ b/code/fms/fms-vue/src/components/fms-module-common/schemaRender.js @@ -10,7 +10,7 @@ * - 列宽/顺序回写(栏位设置保存)→ 更新 view/edit JSON 节点 */ import { loadDataApi } from '@/services/api' -import { parseSchema } from '@/utils/schemaJson' +import { normalizeGroupIds, parseSchema } from '@/utils/schemaJson' /** 需要还原的界面配置类型 */ const RENDER_SCHEMA_TYPES = ['view', 'edit', 'query'] @@ -74,25 +74,21 @@ export function buildListConfig(viewSchema) { })) } -/** 编辑分组 id(JSON 无主键,进入运行时生成临时编码) */ -let groupSequence = 0 -function nextGroupId() { - groupSequence += 1 - return `g${Date.now().toString(36)}${groupSequence.toString(36)}` -} - /** * edit JSON → { groups, rows }(编辑弹窗消费)。 * groups 为分组节点(b_id/b_title/b_xh),rows 为字段行(含栅格/只读/必填/默认值)。 */ export function buildEditRender(editSchema) { + // 分组 id 与配置态同源:老数据缺 id 时按同一规则补齐(确定性纯函数), + // 运行时分组身份与设计态一致,多次渲染结果也稳定 + const base = normalizeGroupIds('edit', editSchema || parseSchema('edit')) const groups = [] const rows = [] const walk = (list, parentGroupId) => { for (const node of list || []) { if (!node || typeof node !== 'object') continue if (node.type === 'group') { - const gid = nextGroupId() + const gid = String(node.id) groups.push({ b_id: gid, b_parent_id: parentGroupId, @@ -125,7 +121,7 @@ export function buildEditRender(editSchema) { }) } } - walk(editSchema?.children || [], null) + walk(base?.children || [], null) rows.forEach((row, index) => { row.b_xh = (index + 1) * 10 }) diff --git a/code/fms/fms-vue/src/components/fms-module-edit/FmsModuleEditModal.vue b/code/fms/fms-vue/src/components/fms-module-edit/FmsModuleEditModal.vue index 7191458f..6db78461 100644 --- a/code/fms/fms-vue/src/components/fms-module-edit/FmsModuleEditModal.vue +++ b/code/fms/fms-vue/src/components/fms-module-edit/FmsModuleEditModal.vue @@ -21,13 +21,15 @@ * ctx = { isNew, record } */ import { computed, ref, watch } from 'vue' -import { Button, Form, FormGroup, FormItem, Message, Modal } from '@/components/ui' +import { Button, Form, FormGroup, FormItem, Message, Modal, Spin } from '@/components/ui' import FmsField from '@/components/fms-field/FmsField.vue' import { loadDataApi, loadDataBySqlApi, nextIdApi, saveObjectApi } from '@/services/api' import { diffRow } from '@/utils/dataChanges' +import { buildGroupI18nKey } from '@/utils/schemaJson' import { identifierRe, isEmptyValue } from '@/components/fms-module-common/queryUtils' import { parseOptions } from '@/components/fms-module-common/fieldUtils' import { loadModuleSchemas, buildEditRender } from '@/components/fms-module-common/schemaRender' +import { useI18nStore } from '@/stores/i18n' const props = defineProps({ /** 受控打开 */ @@ -50,6 +52,8 @@ const props = defineProps({ const emit = defineEmits(['update:open', 'saved']) +const i18nStore = useI18nStore() + /* ================= 元数据状态 ================= */ const configLoading = ref(false) const saving = ref(false) @@ -107,7 +111,17 @@ const sections = computed(() => { const gid = String(group.b_id) const rows = byGroup.get(gid) || [] if (!rows.length) continue - built.push({ key: `g-${gid}`, title: (group.b_title || '').trim(), group, rows }) + // 分组标题:节点手填 i18n 优先,未填按分组 id 派生键(与模块管理页同一规则)取译文; + // 键无译文时回退 b_title,与旧行为一致 + built.push({ + key: `g-${gid}`, + title: i18nStore.t( + group.b_i18n || buildGroupI18nKey(moduleMeta.value?.b_id, 'edit', gid), + (group.b_title || '').trim(), + ), + group, + rows, + }) } const ungrouped = byGroup.get('') || [] if (ungrouped.length) built.push({ key: 'ungrouped', title: '', group: null, rows: ungrouped }) @@ -459,7 +473,9 @@ function controlPropsOf(row) { @update:open="handleClose" @ok="handleSave" > -
加载中...
+
+ +
{{ configError }} diff --git a/code/fms/fms-vue/src/components/fms-module-list/FmsAdvancedQueryPanel.vue b/code/fms/fms-vue/src/components/fms-module-list/FmsAdvancedQueryPanel.vue index e515e353..6bb99e7c 100644 --- a/code/fms/fms-vue/src/components/fms-module-list/FmsAdvancedQueryPanel.vue +++ b/code/fms/fms-vue/src/components/fms-module-list/FmsAdvancedQueryPanel.vue @@ -536,7 +536,7 @@ const previewText = computed(() => describeAdvancedQuery(props.modelValue, itemB border-radius: 4px; background: transparent; color: var(--fms-primary, #0f172b); - font-size: 13px; + font-size: var(--fms-font-size); cursor: pointer; } .filter-add:hover { @@ -573,7 +573,7 @@ const previewText = computed(() => describeAdvancedQuery(props.modelValue, itemB min-width: 0; overflow: hidden; color: var(--fms-text, #1f2329); - font-size: 13px; + font-size: var(--fms-font-size); white-space: nowrap; text-overflow: ellipsis; } diff --git a/code/fms/fms-vue/src/components/fms-module-list/FmsExportDialog.vue b/code/fms/fms-vue/src/components/fms-module-list/FmsExportDialog.vue index aca8bcd0..97877f12 100644 --- a/code/fms/fms-vue/src/components/fms-module-list/FmsExportDialog.vue +++ b/code/fms/fms-vue/src/components/fms-module-list/FmsExportDialog.vue @@ -15,7 +15,7 @@ import { computed, ref, watch } from 'vue' import { Download, GripVertical, RefreshCcw } from '@lucide/vue' import draggable from 'vuedraggable' -import { Button, Input, Message, Modal, Segmented } from '@/components/ui' +import { Button, Input, Message, Modal, Segmented, Spin } from '@/components/ui' import { countExportApi, exportListApi } from '@/services/exportService' const props = defineProps({ @@ -52,9 +52,15 @@ const volumeSize = ref(DEFAULT_VOLUME_SIZE) const total = ref(null) const counting = ref(false) const exporting = ref(false) +let countRequestVersion = 0 const selectedCount = computed(() => fields.value.filter((item) => item.checked).length) -const allChecked = computed(() => fields.value.length > 0 && selectedCount.value === fields.value.length) +const usingListColumns = computed( + () => + fields.value.length > 0 && + fields.value.length === props.columns.length && + fields.value.every((item, index) => item.checked && item.field === props.columns[index].field), +) const volumeCount = computed(() => { if (!total.value || !volumeSize.value || volumeSize.value < 1) return 0 return Math.ceil(total.value / volumeSize.value) @@ -68,7 +74,9 @@ function resetForm() { const savedSet = new Set(saved) fields.value = [ ...saved.map((field) => ({ ...byField.get(field), checked: true })), - ...props.columns.filter((item) => !savedSet.has(item.field)).map((item) => ({ ...item, checked: false })), + ...props.columns + .filter((item) => !savedSet.has(item.field)) + .map((item) => ({ ...item, checked: false })), ] } else { fields.value = props.columns.map((item) => ({ ...item, checked: true })) @@ -86,9 +94,8 @@ function toggleField(field, checked) { /** 全选 = 回到列表列(候选集就是列表可见列),因此顺手清掉个人覆盖 */ function selectAll() { - fields.value = fields.value.map((item) => ({ ...item, checked: true })) + fields.value = props.columns.map((item) => ({ ...item, checked: true })) emit('save-columns', []) - Message.success('已恢复为列表列') } function saveColumns() { @@ -97,7 +104,6 @@ function saveColumns() { return } emit('save-columns', selectedFields()) - Message.success('已保存为我的导出列') } function selectedFields() { @@ -106,26 +112,34 @@ function selectedFields() { async function loadTotal() { if (!props.moduleId) return + const requestVersion = ++countRequestVersion counting.value = true try { - const res = await countExportApi({ moduleId: props.moduleId, searchCondition: props.searchCondition }) + const res = await countExportApi({ + moduleId: props.moduleId, + searchCondition: props.searchCondition, + }) + if (requestVersion !== countRequestVersion) return total.value = Number(res?.data?.total) || 0 } catch (error) { + if (requestVersion !== countRequestVersion) return total.value = null Message.error(error?.message || '统计行数失败') } finally { - counting.value = false + if (requestVersion === countRequestVersion) counting.value = false } } watch( - () => props.open, - (open) => { + () => [props.open, props.moduleId, props.searchCondition], + ([open]) => { + countRequestVersion++ if (!open) return total.value = null resetForm() void loadTotal() }, + { immediate: true }, ) function buildParams() { @@ -199,7 +213,9 @@ async function handleDownload() {
导出列 - + @@ -233,18 +249,40 @@ async function handleDownload() {
导出方式 - +
- + 至 - + 条
每 - - 条一个文件 + + 条一个文件
@@ -252,7 +290,7 @@ async function handleDownload() { diff --git a/code/fms/fms-vue/src/components/fms-table/FmsTable.vue b/code/fms/fms-vue/src/components/fms-table/FmsTable.vue index 774369cf..b0bcf1ac 100644 --- a/code/fms/fms-vue/src/components/fms-table/FmsTable.vue +++ b/code/fms/fms-vue/src/components/fms-table/FmsTable.vue @@ -646,6 +646,8 @@ defineExpose({ :columns="displayColumns" @update:columns="onColumnsUpdate" :data-source="dataSource" + :footer-data="footerData" + :footer-config="footerConfig" :drag-row-config="{ mode: rowDraggable ? 'insert' : 'none' }" :row-key="rowKey" :no-data-full="noDataFull" @@ -708,7 +710,7 @@ defineExpose({ .fms-table__inner.stk-table { th, td { - font-size: 13px; + font-size: var(--fms-font-size); } } @@ -814,7 +816,7 @@ defineExpose({ width: 100%; height: 100%; min-width: 0; - font-size: 13px; + font-size: var(--fms-font-size); } // 内部 Input/Select 默认高度由 --fms-control-height 控制,表格行内需要填满单元格 @@ -920,7 +922,7 @@ defineExpose({ } /* 合计行:与数据区之间加一条分隔线,背景保持与表头一致 */ -:deep(.stk-footer) { +.fms-table .stk-footer { td { border-top: 1px solid var(--fms-border); background: var(--fms-surface, #fff); diff --git a/code/fms/fms-vue/src/components/fms-tree/FmsTree.vue b/code/fms/fms-vue/src/components/fms-tree/FmsTree.vue index f6401df7..be4d8ec1 100644 --- a/code/fms/fms-vue/src/components/fms-tree/FmsTree.vue +++ b/code/fms/fms-vue/src/components/fms-tree/FmsTree.vue @@ -10,8 +10,9 @@ * 4. 加载态(loading)由本组件统一渲染遮罩,调用方不必各自写 Spin 与定位样式。 * * 除下表声明的 props 外,其余 props 与事件(draggable / filter / disabled / contextMenu / - * selectedKeys / checkedKeys / lazy / v-model:selected-keys …)经 $attrs 原样透传给 ui/Tree, - * 事件名与 ui/Tree 保持一致;其中 select 事件额外做了一层归一化,见下。 + * checkedKeys / lazy / v-model:checked-keys …)经 $attrs 原样透传给 ui/Tree, + * 事件名与 ui/Tree 保持一致;其中 selectedKeys 与 select 因为要承载下面的虚拟根归一化, + * 由本组件单独声明,不再走 $attrs。 * * 虚拟根的选中约定(showRoot 时): * - 传入:selectedKeys 为 [] / 空值时,本组件自动把虚拟根当作选中项渲染(高亮「全部分类」); @@ -37,15 +38,12 @@ * * */ -import { computed, ref, useAttrs } from 'vue' +import { computed, ref } from 'vue' import { Spin, Tree } from '@/components/ui' import { translate } from '@/stores/i18n' defineOptions({ inheritAttrs: false }) -/** 未声明的 ui/Tree props(含 selectedKeys)经 $attrs 透传,归一化时需读取原值 */ -const attrs = useAttrs() - /** 虚拟根节点 key:纯内部实现细节,调用方用「空选中键」表达选中根,不需要也不应引用它 */ const ALL_CATEGORY_KEY = '__fms_tree_root__' @@ -64,6 +62,15 @@ const props = defineProps({ rootTitle: { type: String, default: '' }, /** 加载态:树区域盖一层居中 Spin 遮罩(懒加载子节点的局部 loading 仍由 ui/Tree 负责) */ loading: { type: Boolean, default: false }, + /** + * 选中键。undefined 表示非受控:原样交给 ui/Tree 用内部状态维护(点哪个高亮哪个)。 + * 传了数组即受控,选中项完全由调用方决定;showRoot 时的虚拟根归一化见 effectiveSelectedKeys。 + * + * 必须声明为 prop 而不是从 $attrs 取:调用方模板写的是 `:selected-keys`(kebab), + * Vue 放进 $attrs 的键名就是 'selected-keys',用 attrs.selectedKeys 永远取不到值 + * (现有测试用 props 传 camelCase,会绕过这条归一化,测不出问题)。 + */ + selectedKeys: { type: Array, default: undefined }, /** * 展开键。undefined 表示非受控:无虚拟根时原样交给 ui/Tree(default-expand-all 等初始展开配置照常生效), * 有虚拟根时由本组件用内部状态维护,并照常抛出 update:expandedKeys。 @@ -123,13 +130,10 @@ const hasRootKey = (keys) => keys.some((key) => String(key) === String(props.roo * 传入 [] / 空值 → 补成 [rootKey],让虚拟根呈现选中态,调用方用「空」表达「选中全部」; * 传入非空数组 → 原样透传,业务节点的选中完全由调用方决定。 * 只补虚拟根这一个 key,不会顺手选中任何业务节点。 - * - * 注意 selectedKeys 与其他 ui/Tree props 一样走 $attrs(本组件未声明它), - * 因此这里必须从 attrs 取值;若从 props 取会永远拿到 undefined, - * 从而把调用方传的真实选中项覆盖成虚拟根。 + * 未传 selectedKeys(undefined)时保持 undefined,让 ui/Tree 走非受控内部状态。 */ const effectiveSelectedKeys = computed(() => { - const keys = attrs.selectedKeys + const keys = props.selectedKeys if (!props.showRoot) return keys if (keys && keys.length) return keys return [props.rootKey] diff --git a/code/fms/fms-vue/src/components/ui/date/calendar-panel.vue b/code/fms/fms-vue/src/components/ui/date/calendar-panel.vue index a66a8774..9ed4ec13 100644 --- a/code/fms/fms-vue/src/components/ui/date/calendar-panel.vue +++ b/code/fms/fms-vue/src/components/ui/date/calendar-panel.vue @@ -2,6 +2,7 @@ import { computed, nextTick, ref, watch } from 'vue' import dayjs from 'dayjs' import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from '@lucide/vue' +import Button from '../button/button.vue' import TimeSelect from './time-select.vue' import { DEFAULT_DATE_FORMAT, @@ -40,7 +41,7 @@ const props = defineProps({ disabledDate: { type: Function, default: () => false }, }) -const emit = defineEmits(['pick', 'hover', 'update:start', 'update:end']) +const emit = defineEmits(['pick', 'hover', 'update:start', 'update:end', 'confirm']) /* ---------- 日历状态 ---------- */ function anchorDate() { @@ -70,6 +71,11 @@ const calendarLabel = computed(() => `${viewYear.value} 年 ${viewMonth.value + const activeKey = computed(() => dayjs(activeDate.value).format('YYYY-MM-DD')) const isPickingStart = computed(() => !props.start || (props.start && props.end)) +/** 底部「确定」(仅带时间的面板渲染):区间要两端齐、单选要有草稿值才允许提交 */ +const confirmDisabled = computed(() => + props.type === 'range' ? !props.start || !props.end : !props.modelValue, +) + // 单次计算 42 个日期格状态,避免模板渲染中重复调用 disabledDate 和日期比较。 const rows = computed(() => { const today = new Date() @@ -556,5 +562,10 @@ defineExpose({ focusActive })
+ + +
diff --git a/code/fms/fms-vue/src/components/ui/date/date-picker.vue b/code/fms/fms-vue/src/components/ui/date/date-picker.vue index a5f46262..7ae9c4bf 100644 --- a/code/fms/fms-vue/src/components/ui/date/date-picker.vue +++ b/code/fms/fms-vue/src/components/ui/date/date-picker.vue @@ -38,6 +38,8 @@ const draftValue = ref(null) const panelRef = ref(null) const pickerConfig = computed(() => analyzeDateFormat(props.format)) +/** 带时间的面板不在点选时写入,改由面板右下角「确定」提交 */ +const needConfirm = computed(() => pickerConfig.value.hasTime) const resolvedValueFormat = computed(() => hasFormat(props.valueFormat) ? props.valueFormat : pickerConfig.value.primaryFormat, ) @@ -67,8 +69,16 @@ function commit(date) { function handlePick(date) { draftValue.value = normalizeDraft(date) + if (needConfirm.value) return commit(draftValue.value) - if (!pickerConfig.value.hasTime) open.value = false + open.value = false +} + +/** 「确定」:提交草稿并关面板;点外部 / Esc 关闭不经过这里,草稿随之丢弃 */ +function handleConfirm() { + if (!draftValue.value) return + commit(draftValue.value) + open.value = false } function handleClear(event) { @@ -168,7 +178,7 @@ watch(pickerConfig, () => { aria-label="清除日期或时间" @click="handleClear" > -