Files
workspace/code/fms/FMS导出功能设计与进度.md
T
2026-09-16 23:15:06 +08:00

354 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<List<String>>` 表头结构 |
| `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<List<String>>`
(外层 = 叶子列,内层 = 该列自上而下的标题),合并由 `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<String,Object>.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/` 里已不存在)。