354 lines
24 KiB
Markdown
354 lines
24 KiB
Markdown
# 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/` 里已不存在)。
|