319 lines
13 KiB
Markdown
319 lines
13 KiB
Markdown
# M008 Excel 转 PDF 文件转换能力 V1
|
||
|
||
| 项目 | 内容 |
|
||
| --- | --- |
|
||
| 文档状态 | 当前有效 |
|
||
| 适用范围 | 手动上传 Excel 转 PDF、邮件附件 Excel 自动派生 PDF |
|
||
| 推荐方案 | LibreOffice headless 作为后端受控转换引擎 |
|
||
| 当前目标 | CP2 已实现手动上传 Excel 转 PDF 后端接口;邮件附件自动转换、持久化转换任务和 worker 后置 |
|
||
|
||
## 1. 背景
|
||
|
||
系统后续会出现两类 Excel 转 PDF 需求:
|
||
|
||
- 前端 Debug / 后台页面手动上传 `.xls` / `.xlsx`,后端转换为 PDF 后返回下载或 OSS URL。
|
||
- 邮件来源链路中,AgentBus 或 Debug EML 解析到 Excel 附件后,系统自动派生 PDF,便于前端预览、人工复核或后续证据沉淀。
|
||
|
||
这两类场景不应各自实现转换逻辑。第一版应把 Excel 转 PDF 做成平台级文件转换能力,手动上传和自动转换都调用同一套 Service / Adapter。
|
||
|
||
## 2. 设计原则
|
||
|
||
- 文件转换属于 `platform` 通用能力,不绑定 `reservation` 订单任务流程。
|
||
- 业务代码只依赖内部稳定 Service,不直接调用 `soffice` 命令。
|
||
- LibreOffice 作为外部运行时依赖,需要通过 Adapter 隔离。
|
||
- 手动上传接口和邮件附件自动转换共享同一套校验、超时、并发、临时文件清理、OSS 上传和错误处理规则。
|
||
- 转换失败不能影响 SourceMessage 原始邮件入库;自动转换失败只记录失败原因和可重试状态。
|
||
- Excel 原文件和 PDF 派生文件必须能建立来源关系,便于审计和排查。
|
||
- 默认不支持宏执行,不支持外部链接刷新,不支持把 Excel 内容写入普通日志。
|
||
|
||
## 3. 方案选择
|
||
|
||
### 3.1 推荐方案:LibreOffice headless
|
||
|
||
后端把 Excel 文件写入隔离临时目录,然后调用 LibreOffice headless:
|
||
|
||
```bash
|
||
soffice --headless --nologo --nofirststartwizard --norestore \
|
||
-env:UserInstallation=file:///tmp/th-hotel-lo-profile-xxx \
|
||
--convert-to pdf \
|
||
--outdir /tmp/th-hotel-convert-output-xxx \
|
||
/tmp/th-hotel-convert-input-xxx/source.xlsx
|
||
```
|
||
|
||
中文说明:
|
||
|
||
- `soffice` 负责按照 Excel 自身页面设置、分页、合并单元格、图片和样式生成 PDF。
|
||
- `UserInstallation` 必须为每次转换使用独立临时目录,降低并发转换时的 profile 冲突风险。
|
||
- 后端必须设置转换超时;超时后销毁进程并清理临时文件。
|
||
|
||
### 3.2 不推荐第一版使用 POI 手工绘制 PDF
|
||
|
||
Apache POI 可以读取 Excel,但要完整还原页面设置、分页、图表、图片、字体、合并单元格和打印区域,需要大量手工逻辑,维护成本高,不适合作为第一版主方案。
|
||
|
||
### 3.3 商业库作为备选
|
||
|
||
Aspose / Spire 等商业库可以降低运行时部署复杂度,但需要确认授权、费用和商用合规。除非项目明确采购,否则第一版不采用。
|
||
|
||
## 4. 后端模块边界
|
||
|
||
建议新增平台模块:
|
||
|
||
```text
|
||
server/src/main/java/cn/nianxx/thhotel/platform/documentconversion
|
||
├── control
|
||
├── service
|
||
│ └── impl
|
||
├── domain
|
||
├── mapper
|
||
├── repository
|
||
└── common
|
||
├── dto
|
||
├── request
|
||
├── result
|
||
└── enums
|
||
|
||
server/src/main/java/cn/nianxx/thhotel/integrations/document/libreoffice
|
||
└── adapter
|
||
```
|
||
|
||
中文说明:
|
||
|
||
| 模块 | 中文职责 |
|
||
| --- | --- |
|
||
| `platform.documentconversion` | 平台文件转换能力,负责请求校验、转换任务、OSS 输出、审计和结果查询 |
|
||
| `integrations.document.libreoffice.adapter` | LibreOffice 外部进程适配器,只负责调用 `soffice`、收集退出码和输出文件 |
|
||
| `integrations.storage.aliyunoss` | 继续作为 OSS 上传端口,转换服务只依赖 `ObjectStorageService` |
|
||
|
||
后端实现时继续遵守当前项目包结构规范:`control`、`service`、`service.impl`、`domain`、`mapper`、`repository`、`common.request`、`common.result`、`common.dto`、`common.enums`。
|
||
|
||
## 5. 第一版数据流
|
||
|
||
### 5.1 手动上传转换
|
||
|
||
```text
|
||
前端上传 Excel
|
||
-> 后端校验文件类型和大小
|
||
-> 写入隔离临时目录
|
||
-> 调用 LibreOffice headless 转 PDF
|
||
-> 校验 PDF 输出存在且非空
|
||
-> 上传 PDF 到 OSS 或直接返回文件流
|
||
-> 返回转换结果
|
||
-> 清理临时文件
|
||
```
|
||
|
||
建议第一版优先返回 OSS URL,原因:
|
||
|
||
- 与现有 Debug EML 附件处理和阿里云 OSS 上传能力一致。
|
||
- PDF 可以被前端预览或下载。
|
||
- 转换结果可以被后续审计和自动转换链路复用。
|
||
|
||
### 5.2 邮件附件自动转换
|
||
|
||
```text
|
||
SourceMessage 入库 / EML 调试链路解析附件
|
||
-> 识别 Excel 附件
|
||
-> 创建文件转换任务
|
||
-> 异步 worker 执行转换
|
||
-> PDF 上传 OSS
|
||
-> 回写转换任务状态和派生 PDF 结果
|
||
```
|
||
|
||
自动转换不应阻塞邮件入库、AgentBus WebSocket 回调、SuperAgent dispatch 或任务结果入站。第一版如果转换失败,只记录 `FAILED` / `RETRYABLE_FAILED` 和安全错误摘要,不影响原附件可见。
|
||
|
||
## 6. 建议接口边界
|
||
|
||
### 6.1 手动上传接口
|
||
|
||
CP2 已实现:
|
||
|
||
```text
|
||
POST /api/system/document-conversions/excel-to-pdf
|
||
```
|
||
|
||
中文说明:
|
||
|
||
- 接口分类:`FRONTEND_DEBUG` 或 `FRONTEND_ADMIN`,不能作为普通公开接口。
|
||
- 鉴权方式:CP2 使用环境开关 + 受控 access key;后续可统一迁移到登录权限体系。
|
||
- Header:`X-TH-Hotel-Document-Conversion-Key: <文件转换访问口令>`。
|
||
- 请求类型:`multipart/form-data`。
|
||
- 文件字段:`file`。
|
||
- 可选字段:`hotel_id`,用于 OSS 对象路径分组;CP2 不做酒店访问权校验,生产启用前应迁移到登录权限体系。
|
||
- 返回:转换状态、PDF OSS URL、对象路径、文件名、大小、转换耗时、安全错误摘要。
|
||
|
||
请求示例:
|
||
|
||
```bash
|
||
curl -X POST "$BASE_URL/api/system/document-conversions/excel-to-pdf" \
|
||
-H "X-TH-Hotel-Document-Conversion-Key: $DOCUMENT_CONVERSION_ACCESS_KEY" \
|
||
-F "hotel_id=HOTEL-TEST" \
|
||
-F "file=@booking-request.xlsx"
|
||
```
|
||
|
||
成功响应示例:
|
||
|
||
```json
|
||
{
|
||
"conversion_status": "SUCCEEDED",
|
||
"source_file_name": "booking-request.xlsx",
|
||
"source_size_bytes": 12345,
|
||
"pdf_file_name": "booking-request.pdf",
|
||
"pdf_url": "https://oss.example.test/document-conversions/excel-to-pdf/HOTEL-TEST/2026-07-16/.../booking-request.pdf",
|
||
"object_key": "document-conversions/excel-to-pdf/HOTEL-TEST/2026-07-16/.../booking-request.pdf",
|
||
"content_type": "application/pdf",
|
||
"pdf_size_bytes": 67890,
|
||
"duration_millis": 1200
|
||
}
|
||
```
|
||
|
||
错误码:
|
||
|
||
| HTTP | 错误码 | 中文含义 |
|
||
| --- | --- | --- |
|
||
| `401` | `DOCUMENT_CONVERSION_KEY_INVALID` | 文件转换访问口令缺失或错误 |
|
||
| `404` | `DOCUMENT_CONVERSION_DISABLED` | 文件转换接口未启用 |
|
||
| `400` | `DOCUMENT_CONVERSION_FILE_REQUIRED` | 缺少上传文件或文件为空 |
|
||
| `400` | `DOCUMENT_CONVERSION_FILE_TYPE_UNSUPPORTED` | 不是 `.xls` / `.xlsx` |
|
||
| `400` | `DOCUMENT_CONVERSION_FILE_CONTENT_INVALID` | 文件扩展名是 Excel,但文件头不是 `.xlsx` ZIP / `.xls` OLE 格式 |
|
||
| `413` | `DOCUMENT_CONVERSION_FILE_TOO_LARGE` | 超过配置的文件大小限制 |
|
||
| `429` | `DOCUMENT_CONVERSION_BUSY` | 转换并发已满,稍后重试 |
|
||
| `502` | `DOCUMENT_CONVERSION_FAILED` | LibreOffice 转换失败或未生成有效 PDF |
|
||
| `502` | `DOCUMENT_CONVERSION_OSS_UPLOAD_FAILED` | PDF 上传 OSS 失败 |
|
||
| `504` | `DOCUMENT_CONVERSION_TIMEOUT` | LibreOffice 转换超时 |
|
||
|
||
### 6.2 自动转换内部入口
|
||
|
||
建议后续只暴露内部 Service,不提供给前端直接调用:
|
||
|
||
```text
|
||
DocumentConversionService.createExcelToPdfRun(...)
|
||
DocumentConversionWorker.processPendingRuns(...)
|
||
```
|
||
|
||
中文说明:
|
||
|
||
- 邮件链路、Debug EML 链路和未来其他附件来源都只创建转换任务。
|
||
- worker 统一处理重试、超时和状态流转。
|
||
|
||
## 7. 建议数据模型
|
||
|
||
如需要持久化转换记录,建议新增表:
|
||
|
||
```text
|
||
platform_document_conversion_run
|
||
```
|
||
|
||
建议核心字段:
|
||
|
||
| 字段 | 中文含义 |
|
||
| --- | --- |
|
||
| `id` | 内部转换任务 ID |
|
||
| `hotel_id` | 酒店 ID,手动上传或来源消息所属酒店 |
|
||
| `conversion_type` | 转换类型,第一版为 `EXCEL_TO_PDF` |
|
||
| `source_type` | 来源类型,例如 `MANUAL_UPLOAD`、`SOURCE_MESSAGE_ATTACHMENT`、`DEBUG_EML_ATTACHMENT` |
|
||
| `source_object_id` | 来源对象 ID,例如 SourceMessage ID 或 Debug run ID |
|
||
| `source_file_name` | 原始文件名 |
|
||
| `source_content_type` | 原始文件 MIME |
|
||
| `source_oss_url` | 原文件 OSS URL;手动上传如不保留原文件可为空 |
|
||
| `target_file_name` | 生成 PDF 文件名 |
|
||
| `target_oss_url` | 生成 PDF OSS URL |
|
||
| `status` | 转换状态 |
|
||
| `attempt_count` | 已尝试次数 |
|
||
| `safe_error_code` | 安全错误码 |
|
||
| `safe_error_summary` | 安全错误摘要,不包含文件内容和敏感 URL |
|
||
| `created_by` | 创建人或机器身份 |
|
||
| `created_at` / `updated_at` | UTC 时间 |
|
||
|
||
建议状态:
|
||
|
||
| 状态 | 中文含义 |
|
||
| --- | --- |
|
||
| `PENDING` | 等待转换 |
|
||
| `RUNNING` | 正在转换 |
|
||
| `SUCCEEDED` | 转换成功 |
|
||
| `RETRYABLE_FAILED` | 可重试失败 |
|
||
| `FAILED` | 不可重试失败 |
|
||
| `CANCELED` | 已取消 |
|
||
|
||
## 8. 配置与部署要求
|
||
|
||
建议配置项:
|
||
|
||
| 配置 | 是否 Secret | 中文说明 |
|
||
| --- | --- | --- |
|
||
| `DOCUMENT_CONVERSION_DEV_ENABLED` / `DOCUMENT_CONVERSION_TEST_ENABLED` / `DOCUMENT_CONVERSION_PROD_ENABLED` | 否 | 是否启用文件转换能力 |
|
||
| `DOCUMENT_CONVERSION_DEV_ACCESS_KEY` / `DOCUMENT_CONVERSION_TEST_ACCESS_KEY` / `DOCUMENT_CONVERSION_PROD_ACCESS_KEY` | 是 | 手动上传转换访问口令;未配置时可兜底 `DOCUMENT_CONVERSION_ACCESS_KEY` |
|
||
| `DOCUMENT_CONVERSION_SOFFICE_PATH` | 否 | `soffice` 可执行文件路径,默认 `soffice` |
|
||
| `DOCUMENT_CONVERSION_TEMP_DIR` | 否 | 临时文件根目录 |
|
||
| `DOCUMENT_CONVERSION_MAX_FILE_BYTES` | 否 | 单个 Excel 最大字节数 |
|
||
| `DOCUMENT_CONVERSION_TIMEOUT_SECONDS` | 否 | 单次转换超时时间 |
|
||
| `DOCUMENT_CONVERSION_MAX_CONCURRENT` | 否 | 最大并发转换数 |
|
||
| `DOCUMENT_CONVERSION_OUTPUT_OSS_PREFIX` | 否 | PDF 输出 OSS 前缀 |
|
||
| `DOCUMENT_CONVERSION_WORKER_ENABLED` | 否 | 自动转换 worker 是否启用 |
|
||
|
||
服务器需要准备:
|
||
|
||
- 安装 LibreOffice / LibreOffice Calc。
|
||
- 安装中文、英文、泰文等业务需要字体,例如 Noto CJK、Noto Thai、Arial 兼容字体。
|
||
- 确认运行后端的系统用户可以执行 `soffice`,并有临时目录读写权限。
|
||
- 确认临时目录磁盘空间、清理策略和权限隔离。
|
||
- 测试机和生产机 LibreOffice 版本尽量一致,避免同一 Excel 输出 PDF 差异过大。
|
||
|
||
## 9. 安全与稳定性要求
|
||
|
||
- 只允许 `.xls` / `.xlsx`;第一版不支持 `.xlsm`。后端会同时校验扩展名和文件头:`.xlsx` 必须是 ZIP / OOXML 头,`.xls` 必须是 OLE 复合文档头。
|
||
- 文件大小必须有限制,默认建议不超过 20 MB,具体值实现前再确认。
|
||
- 使用 `ProcessBuilder` 参数数组调用外部进程,禁止拼接 shell 字符串。
|
||
- 每次转换使用独立输入目录、输出目录和 LibreOffice profile 目录。
|
||
- 转换完成、失败或超时后必须清理临时文件。
|
||
- 日志只记录转换 ID、文件类型、大小、耗时、状态和安全错误摘要,不记录 Excel 内容、PDF 内容、OSS 签名 URL 或客户敏感信息。
|
||
- 自动转换 worker 必须限制并发,避免大量附件同时转换拖垮后端。
|
||
- 生产环境默认关闭,待测试机验证转换质量、字体和性能后再开启。
|
||
|
||
## 10. Checkpoint 规划
|
||
|
||
### CP1:方案和部署验证
|
||
|
||
- 落地本文档。
|
||
- 测试机安装 LibreOffice 和字体。
|
||
- 用真实业务样式 Excel 手工执行 `soffice` 命令,确认 PDF 保真度。
|
||
- 明确 PDF 输出是直接下载还是 OSS URL,建议第一版 OSS URL。
|
||
|
||
### CP2:手动上传转换接口
|
||
|
||
- 新增平台文件转换模块。
|
||
- 实现 `POST /api/system/document-conversions/excel-to-pdf`。
|
||
- 接入 OSS 输出。
|
||
- 补充文件类型、大小、超时、临时文件清理和错误返回测试。
|
||
- 更新前后端沟通文档和上线注意事项。
|
||
|
||
当前状态:已完成第一版后端接口。CP2 不落库、不做 worker、不自动处理邮件附件;返回的 PDF 会上传 OSS,接口受 `document-conversion.enabled` 和 `X-TH-Hotel-Document-Conversion-Key` 控制。
|
||
|
||
### CP3:持久化转换任务和 worker
|
||
|
||
- 新增 `platform_document_conversion_run`。
|
||
- 实现 PENDING / RUNNING / SUCCEEDED / RETRYABLE_FAILED / FAILED 状态流转。
|
||
- 支持自动重试、错误摘要和结果查询。
|
||
|
||
### CP4:邮件附件自动转换
|
||
|
||
- SourceMessage / Debug EML 链路识别 Excel 附件后创建转换任务。
|
||
- PDF 派生结果和原附件建立来源关系。
|
||
- 自动转换失败不影响原邮件入库和业务主流程。
|
||
|
||
### CP5:前端展示和下载
|
||
|
||
- 手动上传页面展示转换进度和 PDF 链接。
|
||
- 邮件会话或附件区域展示 Excel 原件和派生 PDF。
|
||
- 前端不得把 OSS URL 写入普通日志、埋点或 URL query。
|
||
|
||
## 11. 当前未确定事项
|
||
|
||
实现前需要确认:
|
||
|
||
- 手动上传第一版返回 PDF 文件流,还是上传 OSS 后返回 URL;当前建议 OSS URL。
|
||
- 手动上传接口是放在 Debug 页面、系统管理页面,还是单独工具页面。
|
||
- 第一版最大文件大小和最大转换时长;当前代码默认 20 MB 和 60 秒,可通过环境变量调整。
|
||
- 自动转换是否只处理邮件附件,还是也处理用户手动上传后保存的 Excel。
|
||
- PDF 是否需要长期保存;如果长期保存,需要确认 OSS 生命周期和清理策略。
|
||
- 是否需要把 Excel 原文件也统一转存 OSS;自动附件场景通常已有 OSS URL,手动上传场景需要确认。
|