实现Excel转PDF手动转换接口

This commit is contained in:
andy
2026-07-16 18:25:51 +07:00
parent ed37d5f955
commit 8d53b72363
30 changed files with 1995 additions and 52 deletions

View File

@@ -20,6 +20,7 @@
| `../../README.md` | 当前有效 | 项目根说明记录目录、启动命令、健康检查、Debug EML 和 MCP 基础说明。 |
| `backend-development-guidelines.md` | 当前有效 | 当前项目后端专属规范。 |
| `backend-time-design.md` | 当前有效 | 当前项目时间设计说明,记录数据库 UTC、API `Z` 时间、酒店时区展示和本地日期边界。 |
| `security-access-control-boundary.md` | 当前有效 | 当前项目接口暴露、权限码、酒店隔离和审计边界总表;新增或修改接口时必须同步。 |
| `frontend-development-guidelines.md` | 当前有效 | 当前项目前端专属规范。 |
| `frontend-backend/README.md` | 当前有效 | 前后端协作入口,记录接口契约来源、字段白名单和当前后置事项。 |
| `go-live-notes.md` | 当前有效 | 当前项目上线注意事项记录上线前检查、环境变量、安全、AgentBus、验证和回滚。 |
@@ -42,6 +43,7 @@
| `requirements/M005-hotel-context-unification-plan.md` | 当前有效 | M005 酒店上下文统一收口方案。 |
| `requirements/M006-system-admin-management-console-v1.md` | 草案 | M006 系统管理后台方案,覆盖用户、角色、权限、菜单、酒店和用户酒店授权维护。 |
| `requirements/M007-agentbus-superagent-auto-dispatch-v1.md` | 当前有效 | M007 AgentBus 新邮件入库后异步分发 SuperAgent 的后端 V1 方案,当前默认关闭,等待测试机联调。 |
| `requirements/M008-excel-to-pdf-conversion-v1.md` | 当前有效 | M008 Excel 转 PDF 文件转换能力方案,记录 LibreOffice headless、手动上传转换、邮件附件自动派生 PDF 和部署要求CP2 已实现手动上传后端接口。 |
## 集成契约
@@ -70,6 +72,7 @@
- SuperAgent 对外 HTTP 接口以 `integrations/superagent-api-contract.md` 为权威来源。
- SuperAgent MCP 文档以 `integrations/superagent-mcp/` 为对外交付资料包,但字段语义应跟随 HTTP 总契约。
- 接口暴露、权限、酒店隔离和审计边界以 `security-access-control-boundary.md` 为总检查清单;具体 SuperAgent / MCP / AgentBus 请求响应契约仍以 `integrations/` 下对应文档为准。
- M002 V1 只作为历史参考V2 记录当前阶段实现;后续 M002 新开发以 `requirements/M002-order-task-workflow-v3.md` 为开发基线。
- 前端展示 / 编辑字段以 2026-07-11 P0 冻结基线中的前端字段表、0712 字段控件说明和 `requirements/M002-task-field-control-contract-v1.md` 为白名单和控件契约基线;后端完整校验和 OPERA 映射仍以任务卡完整矩阵、0711 runtime 契约和后端规则为准。
- 时间点语义以 `backend-time-design.md` 为准;数据库时间点按 UTC 理解API 返回带 `Z` 的 UTC 时间,页面再按酒店或用户时区展示。

View File

@@ -65,8 +65,8 @@
| `GET /api/reservation/tasks/{taskId}/audits` | 查询任务审计流水 | 必须带 Bearer token需要 `RESERVATION_AUDIT_READ`,后端按任务所属酒店做访问校验;用于展示人工确认、转换、模拟操作等轨迹。 |
| `GET /api/source-messages` | 查询来源消息安全摘要 | 必须带 Bearer token需要 `SOURCE_MESSAGE_READ`列表不返回邮件正文、HTML、附件 URL 或原始 payload查询参数以 `hotel_id``external_message_id``external_conversation_id``page_num``page_size` 为准,后端暂兼容早期 camelCase 参数。 |
| `GET /api/source-messages/{id}` | 查询来源消息安全详情 | 必须带 Bearer token需要 `SOURCE_MESSAGE_READ`,后端按消息所属酒店做访问校验;只用于安全摘要详情。 |
| `GET /api/source-messages/{id}/original` | 读取来源消息原文 | 需要受控访问头,返回 HTML 时前端展示前必须 sanitize。 |
| `GET /api/source-messages/{sourceMessageId}/conversation` | 读取邮件会话详情 | 返回同一外部会话全部邮件的完整 text/html、`html_body_sanitized`、附件外链、内联图片和关联订单 / 任务摘要;前端不传原文读取 key展示 HTML 时优先使用 `html_body_sanitized`。 |
| `GET /api/source-messages/{id}/original` | 读取来源消息原文 | 必须带 Bearer token需要同时拥有 `SOURCE_MESSAGE_READ``SOURCE_MESSAGE_ORIGINAL_READ`;后端按消息所属酒店做访问校验;返回 HTML 时前端展示前必须 sanitize。 |
| `GET /api/source-messages/{sourceMessageId}/conversation` | 读取邮件会话详情 | 必须带 Bearer token需要同时拥有 `SOURCE_MESSAGE_READ``SOURCE_MESSAGE_ORIGINAL_READ`返回同一外部会话全部邮件的完整 text/html、`html_body_sanitized`、附件外链、内联图片和关联订单 / 任务摘要;前端不传原文读取 key展示 HTML 时优先使用 `html_body_sanitized`。 |
| `POST /api/system/debug/eml-superagent-runs` | Debug 页面上传 `.eml` 并调用 SuperAgent | 仅 dev/test 受控调试使用;会写入 SourceMessage Inbox但不创建订单和任务。 |
| `GET/POST/PUT /api/admin/users...` | 系统管理用户维护 | 需要 Bearer token 和 `SYSTEM_USER_MANAGE`;用户 ID 返回字符串;禁用用户会撤销其 ACTIVE session。 |
| `GET/POST/PUT /api/admin/roles...` | 系统管理角色权限维护 | 需要 `SYSTEM_ROLE_MANAGE`;内置角色只读,自定义角色可新增、编辑和分配权限。 |
@@ -107,7 +107,7 @@ POST /api/auth/logout
- 当前后端已强制拦截第一批 Reservation / SourceMessage 只读接口任务列表、订单列表、订单详情、任务详情、任务审计列表、SourceMessage 摘要列表和摘要详情。调用这些接口必须带 Bearer token。
- 第一批只读接口权限码分别是:`RESERVATION_TASK_READ``RESERVATION_ORDER_READ``RESERVATION_AUDIT_READ``SOURCE_MESSAGE_READ`。前端菜单、按钮和路由守卫应使用 `/api/auth/me` 返回的 `permissions[]``menus[]`
- 后端会按当前登录用户的可访问酒店集合做隔离;显式传 `hotel_id` 时会校验该酒店是否可访问,按 `orderId``taskId``sourceMessageId` 定位的详情接口会反查对象实际所属酒店并校验访问权。
- Reservation 写操作、邮件原文 / conversation 完整正文、Debug / Demo / Replay / Probe 等接口仍按 `../security-access-control-boundary.md` 的分阶段计划继续收口,前端不要自行假设它们和第一批只读接口完全一致
- 邮件原文 / conversation 完整正文接口已完成权限收口,必须带 Bearer token 且同时需要 `SOURCE_MESSAGE_READ``SOURCE_MESSAGE_ORIGINAL_READ`Reservation 写操作、Debug / Demo / Replay / Probe 等接口仍按 `../security-access-control-boundary.md` 的分阶段计划继续收口。
- `/api/auth/me` 返回 `user``default_hotel_id``hotels[]``permissions[]``menus[]`;菜单入口应优先使用 `menus[]`不要继续硬编码订单列表、任务队列、Debug EML。
- `menus[]` 只包含可见菜单;订单详情、任务详情和邮件会话详情是隐藏详情路由,不会作为菜单项返回。
- `DEBUG_EML_SUPERAGENT` 菜单第一版只授予 `SYSTEM_ADMIN`;这只表示页面入口是否可见,不代表后端会把 `X-TH-Hotel-Debug-Upload-Key` 下发给前端。
@@ -131,14 +131,14 @@ POST /api/auth/logout
### 5.4 邮件会话详情接入注意
- `GET /api/source-messages/{sourceMessageId}/conversation` 只接收路径参数 `sourceMessageId`;第一版不接收 `hotelId``includeBody``includeRelated`
- `GET /api/source-messages/{sourceMessageId}/conversation` 只接收路径参数 `sourceMessageId`;第一版不接收 `hotelId``includeBody``includeRelated`;请求必须带 `Authorization: Bearer <access_token>`,且当前用户需要同时拥有 `SOURCE_MESSAGE_READ``SOURCE_MESSAGE_ORIGINAL_READ`
- Reservation 列表、任务列表和订单详情默认不需要前端传 `hotel_id`;如果前端已经接入酒店选择器,可以把当前选中酒店作为可选 `hotel_id` 传给后端。任务详情、任务写操作和邮件会话详情当前仍按对象 ID 定位,不接收该参数。
- 后端会根据 `sourceMessageId` 定位 `external_conversation_id`,并返回同一会话下全部邮件;如果来源消息没有外部会话 ID会降级返回当前单封邮件。
- `messages[]` 按邮件来源接收时间正序返回,前端不要重新按创建时间或任务时间排序。
- 返回内容包含完整 `text_body``html_body``inline_images[]``attachments[]``related_orders[]``related_tasks[]`
- `html_body` 是原始 HTML 兼容字段;`html_body_sanitized` 是后端第一版清洗结果,已移除脚本标签、事件属性和危险协议链接。前端生产展示必须优先使用 `html_body_sanitized`,并可用 `html_render_mode=SANITIZED_HTML` 判断渲染模式。
- 第一版仅处理 HTML 内容安全;`inline_images[]``attachments[]``externalUrl` 来自本系统 OSS 服务暂不做额外拦截但前端仍不得写入普通日志、错误上报、localStorage 或 URL query。
- 会话详情接口由后端内部写原文读取审计,前端不传 `X-TH-Hotel-Source-Original-Read-Key`
- 会话详情接口由后端内部写原文读取审计,actor 使用当前登录用户稳定 ID前端不传 `X-TH-Hotel-Source-Original-Read-Key``X-TH-Hotel-Actor``X-TH-Hotel-Access-Scene`
- 会话详情外层字段主要是 snake_case但媒体对象沿用原文读取接口字段当前是 `mediaType``fileName``contentType``sizeBytes``externalUrl``externalMediaId` 这种 camelCase前端类型定义需要单独处理。
### 5.5 订单列表接入注意
@@ -336,6 +336,47 @@ run_label: 可选调试标签
- 单酒店阶段只允许一家 `ACTIVE` 酒店,后端会拒绝启用第二家 `ACTIVE`,也会拒绝禁用最后一家 `ACTIVE`
- 系统管理写操作会写入 `platform_admin_audit_log`;审计接口 `GET /api/admin/audits` 可按 `target_type``target_id``action` 查询。
### 5.10 Excel 转 PDF 手动上传接口接入注意
后端已提供 M008 CP2 Excel 转 PDF 手动上传接口:
```text
POST /api/system/document-conversions/excel-to-pdf
Header: X-TH-Hotel-Document-Conversion-Key: <文件转换访问口令>
Content-Type: multipart/form-data
file: .xls / .xlsx 文件
hotel_id: 可选;用于 OSS 对象路径分组
```
成功响应:
```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
}
```
前端注意:
- 该接口当前属于受控调试 / 后台工具能力,不是普通公开上传接口。
- `X-TH-Hotel-Document-Conversion-Key` 不能写入 `VITE_*`、源码、构建产物、URL query、localStorage、错误上报或普通日志。
- 只允许上传 `.xls` / `.xlsx``.xlsm` 第一版不支持。后端会做扩展名和文件头轻量校验,改后缀的非 Excel 文件会返回 `DOCUMENT_CONVERSION_FILE_CONTENT_INVALID`
- 后端默认大小限制是 20 MB测试机可以通过环境变量调整。
- `pdf_url` 来自本系统 OSS可用于预览或下载但不要写入普通日志、埋点、错误上报或 URL query。
- `DOCUMENT_CONVERSION_DISABLED` 表示后端未开启文件转换能力,页面应提示联系管理员或切换到已开启环境。
- `DOCUMENT_CONVERSION_BUSY` 表示后端 LibreOffice 并发已满,页面可以提示稍后重试。
- `DOCUMENT_CONVERSION_TIMEOUT` / `DOCUMENT_CONVERSION_FAILED` 通常需要后端排查 LibreOffice、字体、文件格式或临时目录权限。
- CP2 不会创建转换任务记录,也不会自动处理邮件附件;邮件附件自动派生 PDF 是 M008 后续 checkpoint。
## 6. 不给前端直接调用的接口
- `POST /api/system/reservation/demo-data` 只用于 dev/test 联调造数,不是生产业务页面接口;访问口令不能进入前端代码。

View File

@@ -18,6 +18,7 @@
- Reservation OPERA 模拟骨架已确认任务固定生成两条模拟操作支持执行、失败重试、attempt 记录和任务审计列表。
- SuperAgent 查询上下文接口 1、2支持 HMAC 鉴权的订单上下文查询和对象详情查询。
- Debug EML 上传到 SuperAgent 调试链路:受控上传 `.eml`、转存阿里云 OSS、写入 SourceMessage Inbox、调用 SuperAgent Open API 并返回调试结果。
- Excel 转 PDF 手动上传接口:受控上传 `.xls` / `.xlsx`,通过 LibreOffice headless 转 PDF 后上传阿里云 OSS 并返回 PDF URL。
- 登录权限底座:支持用户名密码登录、登出、当前用户上下文、数据库 session token、可访问酒店、权限码和可见菜单。
- 系统管理后台 V1支持用户、角色权限、菜单、酒店和管理操作审计的受控维护接口与前端页面。
@@ -34,6 +35,7 @@
- 现有业务接口强制登录和强制权限拦截。
- 业务审计 actor 全量迁移到当前登录用户。
- Debug EML 上传链路不属于生产普通业务页面能力,生产默认关闭;即使已有登录权限,也不要开放给普通用户。
- Excel 转 PDF 当前只完成手动上传后端接口;邮件附件自动转换、持久化转换任务和 worker 尚未实现。生产默认关闭,启用前必须确认 LibreOffice、字体、OSS、临时目录和访问口令。
## 2. 上线前必须确认
@@ -91,17 +93,13 @@
### 3.3 SourceMessage
| 变量 | 是否 Secret | 上线注意事项 |
| --- | --- | --- |
| `SOURCE_MESSAGE_DEV_ORIGINAL_READ_ACCESS_KEY` | 是 | dev 原文读取临时访问 key。未配置时可兜底使用旧通用变量。 |
| `SOURCE_MESSAGE_TEST_ORIGINAL_READ_ACCESS_KEY` | 是 | test 原文读取临时访问 key。未配置时可兜底使用旧通用变量。 |
| `SOURCE_MESSAGE_PROD_ORIGINAL_READ_ACCESS_KEY` | 是 | prod 原文读取临时访问 key。未配置时原文读取默认关闭。 |
| `SOURCE_MESSAGE_ORIGINAL_READ_ACCESS_KEY` | 是 | 旧通用原文读取 key仅作为兼容兜底。 |
SourceMessage 原文和邮件会话完整正文已迁移到登录权限体系:
注意:
- 原文读取 key 不是用户体系,后续接入正式登录和角色权限后应替换
- 任何能读取原文的调用都必须有调用方和访问场景,并写入审计表
- 前端请求必须携带 `Authorization: Bearer <access_token>`
- 当前用户必须同时拥有 `SOURCE_MESSAGE_READ``SOURCE_MESSAGE_ORIGINAL_READ`
- 后端按 SourceMessage 实际所属酒店校验酒店访问权
- 后端内部写入原文读取审计actor 使用当前登录用户稳定 ID
-`SOURCE_MESSAGE_DEV_ORIGINAL_READ_ACCESS_KEY``SOURCE_MESSAGE_TEST_ORIGINAL_READ_ACCESS_KEY``SOURCE_MESSAGE_PROD_ORIGINAL_READ_ACCESS_KEY``SOURCE_MESSAGE_ORIGINAL_READ_ACCESS_KEY` 已废弃,不再作为部署必备 Secret。
### 3.4 AgentBus
@@ -201,6 +199,31 @@
- Debug EML 和 AgentBus 自动分发复用同一个 SuperAgent Open API SSE 稳定客户端;上线前必须验证 `run.completed + end + final answer` 严格成功条件和 EOF 后 `/events` 恢复。
- 当前共享 Open API client 会自动携带临时 CSRF double-submit header / cookie如果测试机仍返回 `CSRF token missing`,优先检查部署包版本和反向代理是否转发 `X-CSRF-Token``Cookie`
### 3.7 Excel 转 PDF / LibreOffice
| 变量 | 是否 Secret | 上线注意事项 |
| --- | --- | --- |
| `DOCUMENT_CONVERSION_DEV_ENABLED` / `DOCUMENT_CONVERSION_TEST_ENABLED` / `DOCUMENT_CONVERSION_PROD_ENABLED` | 否 | 是否启用 Excel 转 PDF 手动上传接口prod 默认必须保持 `false`,确认运行环境后再开启。 |
| `DOCUMENT_CONVERSION_DEV_ACCESS_KEY` / `DOCUMENT_CONVERSION_TEST_ACCESS_KEY` / `DOCUMENT_CONVERSION_PROD_ACCESS_KEY` | 是 | 文件转换访问口令;未配置时可兜底 `DOCUMENT_CONVERSION_ACCESS_KEY`。不得进入前端源码、镜像、普通日志或文档真实值。 |
| `DOCUMENT_CONVERSION_SOFFICE_PATH` / `DOCUMENT_CONVERSION_*_SOFFICE_PATH` | 否 | `soffice` 可执行文件路径,默认 `soffice`。测试机和生产机路径可能不同。 |
| `DOCUMENT_CONVERSION_TEMP_DIR` / `DOCUMENT_CONVERSION_*_TEMP_DIR` | 否 | 文件转换临时目录。后端运行用户必须有读写权限,目录应有系统清理策略。 |
| `DOCUMENT_CONVERSION_MAX_FILE_BYTES` / `DOCUMENT_CONVERSION_*_MAX_FILE_BYTES` | 否 | 单个 Excel 文件大小上限,默认 `20971520`。 |
| `DOCUMENT_CONVERSION_TIMEOUT_SECONDS` / `DOCUMENT_CONVERSION_*_TIMEOUT_SECONDS` | 否 | 单次 LibreOffice 转换超时秒数,默认 `60`。 |
| `DOCUMENT_CONVERSION_MAX_CONCURRENT` / `DOCUMENT_CONVERSION_*_MAX_CONCURRENT` | 否 | 最大并发转换数,默认 `2`。不要盲目调大,避免 LibreOffice 进程拖垮后端。 |
| `DOCUMENT_CONVERSION_OUTPUT_OSS_PREFIX` / `DOCUMENT_CONVERSION_*_OUTPUT_OSS_PREFIX` | 否 | PDF 输出 OSS 前缀,默认 `document-conversions/excel-to-pdf/`。 |
| `DOCUMENT_CONVERSION_WORKER_ENABLED` / `DOCUMENT_CONVERSION_*_WORKER_ENABLED` | 否 | 自动转换 worker 预留开关CP2 不使用,默认关闭。 |
| `DOCUMENT_CONVERSION_MULTIPART_MAX_FILE_BYTES` / `DOCUMENT_CONVERSION_MULTIPART_MAX_REQUEST_BYTES` | 否 | Spring multipart 框架上限;默认分别为 `25165824` / `29360128`,应高于业务 `MAX_FILE_BYTES`,否则请求会在进入 Controller 前被框架拦截。 |
注意:
- 服务器必须安装 LibreOffice / LibreOffice Calc并确认后端运行用户可以执行 `soffice`
- 服务器必须安装中文、英文、泰文等业务字体;缺少字体会导致 PDF 乱码、缺字或分页变化。
- 每次转换会创建独立临时目录和 LibreOffice profile正常结束、失败或超时后后端会清理但仍建议运维配置临时目录兜底清理。
- 当前接口路径为 `POST /api/system/document-conversions/excel-to-pdf`Header 为 `X-TH-Hotel-Document-Conversion-Key`
- 当前接口只支持 `.xls` / `.xlsx`,不支持 `.xlsm`;后端会校验扩展名和文件头,改后缀的非 Excel 文件会返回受控错误。
- PDF 上传到阿里云 OSS返回 `pdf_url``object_key``pdf_file_name``pdf_size_bytes``duration_millis`
- CP2 不落库,不提供转换历史查询;如果需要自动处理邮件附件,应先进入 M008 后续持久化任务和 worker checkpoint。
## 4. 数据库上线注意事项
当前 SourceMessage 相关 migration
@@ -308,9 +331,8 @@ SourceMessage 普通列表和普通详情只能返回安全摘要:
```text
GET /api/source-messages/{id}/original
Header: X-TH-Hotel-Source-Original-Read-Key
Header: X-TH-Hotel-Actor
Header: X-TH-Hotel-Access-Scene
Header: Authorization: Bearer <access_token>
Required permission: SOURCE_MESSAGE_READ + SOURCE_MESSAGE_ORIGINAL_READ
```
前端展示 `htmlBody` 前必须 sanitize。后端返回 `htmlSanitizeRequired=true` 是提醒前端不要直接信任 HTML。
@@ -418,15 +440,13 @@ GET /api/source-messages/{id}/original
```text
AGENTBUS_PROBE_ENABLED=false
AGENTBUS_CAPTURE_ENABLED=false
SOURCE_MESSAGE_PROD_ORIGINAL_READ_ACCESS_KEY=
SOURCE_MESSAGE_ORIGINAL_READ_ACCESS_KEY=
```
说明:
- 关闭 `AGENTBUS_PROBE_ENABLED` 可以停止 WebSocket 入站连接。
- 关闭 `AGENTBUS_CAPTURE_ENABLED` 可以保留连接但暂停写入 Inbox。
- 清空当前 profile 对应的 `SOURCE_MESSAGE_*_ORIGINAL_READ_ACCESS_KEY`,且不配置旧通用变量,可以关闭原文读取接口
- 如需临时关闭前端邮件原文 / 会话完整正文读取,应从相关角色移除 `SOURCE_MESSAGE_ORIGINAL_READ``SOURCE_MESSAGE_READ`,或禁用对应用户入口;旧原文读取 key 已废弃,不能作为关闭开关
数据库回滚注意:

View File

@@ -0,0 +1,318 @@
# 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手动上传场景需要确认。

View File

@@ -61,8 +61,8 @@
| --- | --- | --- | --- | --- |
| `GET /api/source-messages` | `FRONTEND_USER` | 已强制 Bearer 登录 + `SOURCE_MESSAGE_READ`;列表条件中的酒店按当前用户可访问酒店校验 | 保持登录 + `SOURCE_MESSAGE_READ` + 酒店访问权 | 只读摘要不写审计 |
| `GET /api/source-messages/{id}` | `FRONTEND_USER` | 已强制 Bearer 登录 + `SOURCE_MESSAGE_READ`;按消息实际所属酒店校验访问权 | 保持登录 + `SOURCE_MESSAGE_READ` + 消息所属酒店访问权 | 只读摘要不写审计 |
| `GET /api/source-messages/{id}/conversation` | `FRONTEND_USER` | 返回会话完整 text/html 和媒体 URL;后端内部写原文读取审计 | 登录 + `SOURCE_MESSAGE_READ`,如返回完整正文则还需 `SOURCE_MESSAGE_ORIGINAL_READ` | 必须写原文读取审计 |
| `GET /api/source-messages/{id}/original` | `FRONTEND_USER` | 当前使用受控原文读取 key | 迁移为登录 + `SOURCE_MESSAGE_ORIGINAL_READ` + 酒店访问权access key 仅作兼容或关闭 | 必须写原文读取审计 |
| `GET /api/source-messages/{id}/conversation` | `FRONTEND_USER` | 已强制 Bearer 登录 + `SOURCE_MESSAGE_READ` + `SOURCE_MESSAGE_ORIGINAL_READ`;按消息实际所属酒店校验访问权;返回会话完整 text/html 和媒体 URL | 保持登录 + `SOURCE_MESSAGE_READ` + `SOURCE_MESSAGE_ORIGINAL_READ` + 消息所属酒店访问权 | 必须写原文读取审计actor 使用当前登录用户稳定 ID |
| `GET /api/source-messages/{id}/original` | `FRONTEND_USER` | 已强制 Bearer 登录 + `SOURCE_MESSAGE_READ` + `SOURCE_MESSAGE_ORIGINAL_READ`;按消息实际所属酒店校验访问权;不再使用原文读取 access key | 保持登录 + `SOURCE_MESSAGE_READ` + `SOURCE_MESSAGE_ORIGINAL_READ` + 消息所属酒店访问权 | 必须写原文读取审计actor 使用当前登录用户稳定 ID |
### 3.4 系统管理后台接口
@@ -82,6 +82,7 @@
| `POST /api/system/debug/eml-superagent-runs` | `FRONTEND_DEBUG` | 环境开关 + `X-TH-Hotel-Debug-Upload-Key` | dev/test 可保留 access key长期目标为登录 + `SYSTEM_DEBUG_EML_RUN` + 环境开关 | 写 Debug run必要时补管理 / 调试审计 |
| `GET /api/system/debug/eml-superagent-runs/{runId}` | `FRONTEND_DEBUG` | 环境开关 + access key | 登录 + `SYSTEM_DEBUG_EML_RUN`;避免向普通用户暴露 AI 原始结果 | 只读调试可记录访问日志 |
| `POST /api/system/debug/eml-superagent-runs/stream` | `FRONTEND_DEBUG` | 环境开关 + access key | 登录 + `SYSTEM_DEBUG_EML_RUN`;生产默认关闭 | 写 Debug run 和安全错误摘要 |
| `POST /api/system/document-conversions/excel-to-pdf` | `FRONTEND_DEBUG` | 环境开关 + `X-TH-Hotel-Document-Conversion-Key`;只支持 `.xls` / `.xlsx`PDF 输出到 OSS | 长期目标为登录 + 文件转换调试权限 + 环境开关;生产默认关闭 | CP2 不落库;必要时通过网关访问日志和 OSS 对象路径追踪,后续自动转换任务落库后补转换审计 |
| `GET /api/system/agentbus-probe` | `FRONTEND_DEBUG` | 当前系统状态接口 | 登录 + `SYSTEM_AGENTBUS_PROBE_READ` 或系统管理入口权限 | 不返回 Token、raw frame 或邮件正文 |
| `POST /api/system/reservation/demo-data` | `FRONTEND_DEBUG` | 环境开关 + demo access key | dev/test 使用;生产必须关闭 | 写入演示数据时建议记录调试审计 |
@@ -122,7 +123,7 @@
| `RESERVATION_OPERA_SIM_EXECUTE` | 执行或重试 OPERA 模拟 / 未来真实操作 | OPERA execute / retry |
| `RESERVATION_AUDIT_READ` | 查看业务审计流水 | 任务审计列表 |
| `SOURCE_MESSAGE_READ` | 查看来源邮件安全摘要 | SourceMessage 列表、详情、会话摘要 |
| `SOURCE_MESSAGE_ORIGINAL_READ` | 查看邮件正文、HTML 和附件外链 | original / conversation 完整正文 |
| `SOURCE_MESSAGE_ORIGINAL_READ` | 查看邮件正文、HTML 和附件外链 | original / conversation 完整正文;必须叠加 `SOURCE_MESSAGE_READ` 使用 |
| `SYSTEM_DEBUG_EML_RUN` | 使用 Debug EML 调试链路 | Debug EML 上传、查询、stream |
| `SYSTEM_AGENTBUS_PROBE_READ` | 查看 AgentBus 安全状态 | AgentBus probe |
@@ -133,6 +134,31 @@
3. `docs/project/security-access-control-boundary.md`
4. 前后端协作文档中对应页面按钮或菜单说明。
### 4.1 新增接口权限固定流程
后续新增需要前端用户或管理员调用的接口时,必须按同一套流程维护权限,避免“接口能调用但系统设置里管不了”或“前端隐藏了但后端没拦”的不一致。
固定流程:
1. **确认接口分类。** 先判断接口属于 `FRONTEND_USER``FRONTEND_ADMIN``FRONTEND_DEBUG`、第三方机器接口还是 `INTERNAL_ONLY`。只有前端用户和管理员接口进入用户角色权限模型SuperAgent、AgentBus、MCP 继续使用机器鉴权,不使用用户 Bearer 权限码。
2. **定义稳定权限码。**`PlatformPermissionCode` 增加稳定英文权限码,例如 `RESERVATION_TASK_ASSIGN`。权限码只表达能力边界,不绑定中文文案、不绑定某个按钮样式。
3. **补启动同步元数据。** 在权限启动同步逻辑中补权限名称、权限分组和状态,确保 `platform_permission` 能自动拥有该权限码。
4. **补内置角色默认矩阵。** 明确 `SYSTEM_ADMIN`、业务操作员、只读角色等内置角色是否默认拥有该权限。内置角色矩阵仍以代码为准;自定义角色后续通过系统设置页面分配。
5. **后端接口强制校验。** 在 Controller 或统一入口中显式调用对应鉴权服务,例如 `requirePermission(PlatformPermissionCode.X.name())`。只读接口同时校验对象所属酒店;写接口还要校验状态、幂等、事务和审计。
6. **前端类型和交互同步。** 在前端权限类型中加入新权限码,路由、菜单、按钮和操作入口按 `/api/auth/me` 返回的 `permissions[]` 控制展示。前端控制只提升体验,不能替代后端权限校验。
7. **系统设置可分配。** 权限启动同步后,系统设置的角色权限页面应能看到该权限码;需要给自定义角色授权时,通过系统设置勾选,用户重新登录或刷新上下文后生效。
8. **补测试。** 至少覆盖无 token、无权限、有权限、跨酒店或对象归属校验。第三方接口要补“不被用户登录拦截误伤”的回归测试。
9. **补文档。** 本文矩阵中登记接口分类、权限码、酒店隔离和审计要求;影响前端时同步 `docs/project/frontend-backend/backend-to-frontend-notes.md`;影响 SuperAgent / MCP / AgentBus 时同步对应集成契约。
最小验收口径:
- 新接口没有 token 时返回该分类约定的 401。
- 已登录但缺权限时返回该分类约定的 403。
- 有权限但访问无权酒店或无权对象时返回酒店 / 对象访问拒绝。
- 权限码能在系统设置角色权限页面看到并分配给自定义角色。
- 前端只做路由、菜单、按钮显示控制,后端仍能拦截直接调用。
- 第三方机器接口不被用户 Bearer 权限模型误拦截。
## 5. 酒店隔离规则
- 前端用户接口必须从当前登录用户解析可访问酒店集合。
@@ -184,7 +210,7 @@
后续开发权限收口时建议按以下顺序推进:
1. 已完成 CP1Reservation / SourceMessage 第一批只读查询接口已收口登录、权限码和酒店访问权包括任务列表、订单列表、订单详情、任务详情、任务审计列表、SourceMessage 摘要列表和摘要详情。
2. 再收口邮件原文和会话完整正文读取迁移到 `SOURCE_MESSAGE_ORIGINAL_READ`,保留审计
2. 已完成 CP2邮件原文和会话完整正文读取迁移到 `SOURCE_MESSAGE_READ` + `SOURCE_MESSAGE_ORIGINAL_READ`,并按消息所属酒店校验访问权;原文读取审计 actor 使用当前登录用户稳定 ID
3. 再收口 Reservation 写操作草稿、确认、人工复核、OPERA 模拟。
4. 迁移业务审计 actor 到当前登录用户。
5. 最后处理 Debug、Demo、Replay、AgentBus Probe 等系统调试入口。