# 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/` 里已不存在)。