Files
th-hotel-simple/docs/project/requirements/M010-rooming-list-excel-generation-v1.md
2026-07-22 15:29:33 +07:00

360 lines
16 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.

# M010 Rooming List Excel 生成 V1
| 项目 | 内容 |
| --- | --- |
| 文档状态 | 后端 CP1 已实现CP2 已实现旅游日期派生、Adults 系统计算和目标默认值字段收口 |
| 适用范围 | 从旅行团名单 Excel 解析护照姓名,并生成酒店 / PMS 可导入的 Rooming List Excel |
| 当前目标 | CP1 已支持前端上传来源 Excel、填写分房和目标列默认值后端同步生成目标格式 `.xlsx`CP2 收口为后端从来源 `旅游日期` 派生入住 / 离店日期,成人数由系统按分房结果计算,目标默认值区域只保留付款方式和国籍 |
| 依赖能力 | 登录权限、酒店上下文、Apache POI 或等价 Excel 读写能力 |
## 1. 背景
当前系统已经支持 Manual Invoice 生成,但运营中还存在另一类独立工具诉求:旅行社或渠道提供一份名单 Excel里面包含旅客护照姓名酒店侧需要整理成 PMS / 导入模板要求的 Rooming List Excel。
本功能第一版只解决“来源名单到目标 Rooming List”的结构转换和分房填表问题不依赖订单、任务、SuperAgent、AgentBus 或 OPERA。
M002 V4 `ROOMING_LIST` 任务卡与本工具是两个独立能力V4 卡第一版只做 Rooming List 事项确认,不调用本工具、不解析名单、不生成 Excel、不导入 PMS。
## 2. 第一阶段定位
CP1 已做到:
- 前端上传 `.xls` / `.xlsx` 来源名单。
- 后端解析来源名单中的护照姓名字段。
- 前端输入每间房人数。
- 前端填写或选择目标 Excel 中除姓名外的字段。
- 后端按来源名单顺序分组,生成目标格式 Excel。
- 浏览器直接下载生成后的 `.xlsx` 文件。
CP2 目标调整:
- 前端继续上传 `.xls` / `.xlsx` 来源名单。
- 前端继续输入每间房人数和目标 `Room Type`
- 后端从来源 Excel 的 `旅游日期` 列解析入住日期和离店日期,不再要求前端手填 `arrival` / `departure`
- 后端按分房结果计算目标 `Adults`,不再要求前端手填 `adults`
- 目标默认值区域只保留 `Payment Type``Nationality` 两个字段。
- `Payment Type` 前端默认为 `CA`,后端也按 `CA` 作为当前唯一允许值校验。
- `Nationality` 前端只允许选择 `KR``CHN`,后端按同一枚举校验。
第一阶段不做:
- 不落业务订单。
- 不创建任务。
- 不上传 OSS。
- 不新增生成记录表。
- 不做历史查询和重下载。
- 不接真实 OPERA / OHIP。
- 不做复杂房型混住、拖拽换房或手工调整入住人顺序。
- 不从任务详情或订单详情自动预填字段。
## 3. 与现有模块关系
本功能属于 Reservation 运营工具,不属于 M008 通用文件转换,也不属于 M009 Manual Invoice。
边界说明:
- M008 负责 Excel 转 PDF面向平台文件转换和调试能力。
- M009 负责 Proforma Invoice 生成,输出 PDF 并上传 OSS。
- M010 负责 Rooming List Excel 生成,第一版只输出 `.xlsx` 文件流。
因此第一版建议在 Reservation 模块内新增独立接口、权限码和服务,不复用发票生成权限,也不让前端调用 M008 调试接口。
## 4. 来源 Excel 解析规则
基于当前样例来源文件,第一版默认识别以下表头:
| 来源字段 | 中文说明 | 第一版用途 |
| --- | --- | --- |
| `护照全名` | 护照姓名,可能包含 `/` 或空格 | 解析为目标 Excel 的 `Name``First Name` |
| `旅游日期` | 旅游团起止日期,例如 `2026年5月9日5月14日` | CP2 用于派生目标 Excel 的 `Arrival``Departure` |
CP1 只使用 `护照全名` 生成目标姓名字段。CP2 额外使用 `旅游日期` 派生目标 `Arrival` / `Departure`。来源文件中的证件号、生日、性别、年龄等字段暂不自动写入目标 Excel除非后续单独确认。
### 4.1 姓名拆分
支持的输入格式:
```text
LI/CHUNHONG
LI CHUNHONG
LIU/JIAYI
```
规则:
- 先 trim 首尾空格。
- 优先按 `/` 拆分。
- 如果没有 `/`,按连续空白拆分。
- 第一段写入目标 `Name`
- 剩余部分合并后写入目标 `First Name`
- 如果无法拆出两段,返回明确校验错误,由前端提示用户修正来源 Excel 或后续增加字段映射。
中文说明:
- 当前目标模板中 `Name` 更接近姓氏字段,`First Name` 更接近名字段。
- 第一版不尝试判断中文、韩文、英文姓名顺序,也不做大小写改写。
### 4.2 旅游日期解析
CP2 起后端应识别来源 Excel 的 `旅游日期` 表头,并从有名单数据的行中读取日期区间。
已确认样例:
```text
2026年5月9日5月14日
```
解析规则:
- 左侧起始日期必须能解析出年、月、日。
- 右侧结束日期可以省略年份;省略年份时继承起始日期年份。
- 右侧结束日期可以省略月份;省略月份时继承起始日期月份。
- 支持常见区间分隔符,例如 ```-````—``至``到`
- `Arrival` 使用起始日期,格式化为 `yyyy-MM-dd`
- `Departure` 使用结束日期,格式化为 `yyyy-MM-dd`
- 旅游日期属于酒店本地业务日期,不按 UTC 时间点换算。
- 同一来源文件中参与生成的名单行必须解析出同一个旅游日期区间如果出现多个不同区间CP2 先返回受控错误,不自动混合生成。
- 如果缺少 `旅游日期` 表头、旅游日期为空、格式无法解析或结束日期不晚于起始日期,返回 `ROOMING_LIST_SOURCE_FILE_INVALID` 或统一受控业务错误,不应生成半成品 Excel。
中文说明:
- 当前样表第 2 行表头中包含 `旅游日期`,第 3 行起数据为 `2026年5月9日5月14日`,可派生 `Arrival=2026-05-09``Departure=2026-05-14`
- CP2 暂不支持同一个文件中多批次、多旅游日期混合分房;如后续出现该真实场景,需要先设计按批次拆文件或按旅游日期分组生成多个 Sheet / 多个文件。
## 5. 分房生成规则
前端需要传入 `people_per_room`,表示每间房人数。
规则:
- 按来源 Excel 中旅客出现顺序分组。
- `room_count = ceil(total_people / people_per_room)`
- 每组第一位旅客写入目标 Excel 的 `Name``First Name`
- 同组剩余旅客写入目标 Excel 的 `Accompanying Guests`
- 多个陪同人使用英文逗号 `,` 分隔。
- 最后一组人数不足 `people_per_room` 时,仍按一间房生成一行。
- CP2 起目标 `Adults` 固定由当前房间实际人数派生;例如最后一间尾房只有 2 人时,该行 `Adults=2`,不再使用前端传入的统一成人数覆盖。
- CP2 起目标 `Children` 第一版固定为 `0`,不在前端目标默认值区域展示。
示例:
```text
people_per_room = 3
第 1 组:
Name = LI
First Name = CHUNHONG
Accompanying Guests = ZHANG GAILI, LI HONG
第 2 组:
Name = LIU
First Name = JIAYI
Accompanying Guests = ZHU XINGTONG, CHEN YUCHAI
```
## 6. 目标 Excel 字段
目标 Excel 按当前样例模板生成以下列:
| 目标列 | 第一版来源 |
| --- | --- |
| `Line` | 后端按生成行号从 1 递增 |
| `Name` | 每间房第一位旅客的姓氏 / 第一段姓名 |
| `First Name` | 每间房第一位旅客的名字 / 剩余姓名 |
| `Title` | CP2 固定为空,后续如有稳定默认值再单独确认 |
| `Arrival` | CP2 从来源 Excel `旅游日期` 起始日派生 |
| `Departure` | CP2 从来源 Excel `旅游日期` 结束日派生 |
| `Room Type` | 前端传入 |
| `Rate Code` | CP2 固定为空,后续如需 Rate Code 选择再单独确认 |
| `Number of Rooms` | 第一版每行固定为 `1`,后续如目标模板变化再调整 |
| `Adults` | CP2 由后端按当前房间实际人数派生 |
| `Children` | CP2 固定为 `0` |
| `Payment Type` | 前端选择;当前默认和唯一允许值为 `CA` |
| `VIP` | CP2 固定为空,后续如有稳定默认值再单独确认 |
| `Accompanying Guests` | 同房其他旅客姓名,英文逗号分隔 |
| `Nationality` | 前端选择;当前只允许 `KR``CHN` |
| `Email` | CP2 固定为空,后续如有稳定来源再单独确认 |
| `ID Type` | CP2 固定为空,后续如有稳定来源再单独确认 |
| `ID Number` | CP2 固定为空,后续如有稳定来源再单独确认 |
中文说明:
- CP2 目标默认值区域只保留 `Payment Type``Nationality`,不再展示 `Title``Rate Code``Adults``Children``VIP``Email``ID Type``ID Number`
- `Arrival` / `Departure` 的业务来源是来源名单 Excel 的 `旅游日期`,前端不再手填。
- `Adults` 的业务来源是系统分房结果,前端不再手填。
- 如后续需要恢复 Rate Code、Title、Email 或证件字段,需要单独确认字段来源、默认值和安全边界,不能直接把 CP1 自由文本框恢复为生产能力。
## 7. 接口契约
第一版已新增业务接口,不复用 M008 文件转换接口:
```text
POST /api/reservation/rooming-lists/generations
Authorization: Bearer <access_token>
Content-Type: multipart/form-data
```
表单字段:
| 字段 | 类型 | 必填 | 中文说明 |
| --- | --- | --- | --- |
| `file` | file | 是 | 来源 Excel 文件,仅支持 `.xls` / `.xlsx` |
| `hotel_id` | string | 否 | 酒店 ID不传时按当前登录用户默认酒店或单酒店上下文解析 |
| `people_per_room` | integer | 是 | 每间房人数,必须大于 0 |
| `room_type` | string | 是 | 目标 Excel 的 `Room Type` |
| `payment_type` | string | 否 | CP2 目标 Excel 的 `Payment Type`;为空时后端按 `CA` 处理;当前只允许 `CA` |
| `nationality` | string | 是 | CP2 目标 Excel 的 `Nationality`;当前只允许 `KR``CHN` |
CP2 不再接收以下前端业务字段作为生成依据:
| 字段 | CP2 处理口径 |
| --- | --- |
| `arrival` | 不由前端提交;后端从来源 Excel `旅游日期` 派生 |
| `departure` | 不由前端提交;后端从来源 Excel `旅游日期` 派生 |
| `title` | 固定为空 |
| `rate_code` | 固定为空 |
| `adults` | 后端按当前房间实际人数派生 |
| `children` | 固定为 `0` |
| `vip` | 固定为空 |
| `email` | 固定为空 |
| `id_type` | 固定为空 |
| `id_number` | 固定为空 |
成功响应:
```text
HTTP 200
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Content-Disposition: attachment; filename="rooming-list-<timestamp>.xlsx"
```
错误响应沿用后端统一错误模型,例如:
```json
{
"error_code": "ROOMING_LIST_SOURCE_FILE_INVALID",
"message": "来源名单文件不合法。",
"details": [
"未找到表头:护照全名"
]
}
```
已收口的第一版错误场景:
- multipart 必填字段缺失,返回 `ROOMING_LIST_VALIDATION_FAILED`
- `people_per_room` 数字格式错误,返回 `ROOMING_LIST_VALIDATION_FAILED`
- `payment_type``CA`,返回 `ROOMING_LIST_VALIDATION_FAILED`
- `nationality``KR` / `CHN`,返回 `ROOMING_LIST_VALIDATION_FAILED`
- 来源文件缺失、非 `.xls` / `.xlsx`、无法读取、缺少 `护照全名` 表头或缺少 / 无法解析 `旅游日期` 表头,返回 `ROOMING_LIST_SOURCE_FILE_INVALID`
- 同一来源文件出现多个不同旅游日期区间CP2 返回受控错误,不生成 Excel。
- 错误详情只返回字段名或行号,不回显完整名单、证件号或源文件内容。
## 8. 权限、安全和审计
接口分类:`FRONTEND_USER`
已新增权限码:
```text
RESERVATION_ROOMING_LIST_GENERATE
```
管控要求:
- 必须登录。
- 必须拥有 `RESERVATION_ROOMING_LIST_GENERATE` 权限。
- 上传接口在 multipart 参数绑定前执行登录和权限前置校验,未登录或无权限请求不会进入来源文件解析。
- 必须校验用户对 `hotel_id` 的访问权。
- 第一版直接下载、不落库,因此不写生成记录表。
- 当前 CP1 不新增持久化审计表;后续如增加生成记录或历史下载,再补业务审计落库。
- 日志和错误响应不得输出完整名单、证件号、源文件内容或生成文件内容。
## 9. 后端实现拆分
已实现类职责:
| 类 / 能力 | 中文职责 |
| --- | --- |
| `ReservationRoomingListGenerationController` | 接收前端上传请求,做登录、权限和酒店上下文校验 |
| `ReservationRoomingListGenerationService` | 编排来源解析、分房、目标 Excel 渲染和文件响应 |
| `ReservationRoomingListGenerationServiceImpl` | 具体业务实现 |
| `RoomingListSourceExcelParser` | CP1 解析来源 Excel提取护照姓名CP2 需扩展为同时提取并校验统一旅游日期区间 |
| `RoomingListNameParser` | 拆分 `/` 或空格格式的护照姓名 |
| `RoomingListGroupingService` | 按每房人数分组并生成目标行模型 |
| `RoomingListExcelRenderer` | 生成目标格式 `.xlsx` |
| `ReservationRoomingListGenerationRequest` | multipart 外的业务参数请求模型 |
| `ReservationRoomingListGeneratedFile` | 生成后的下载文件字节和文件名 |
| `ReservationRoomingListGuestDto` | 解析后的旅客姓名结构 |
| `ReservationRoomingListRoomDto` | 分房后的目标行结构 |
中文说明:
- 第一版不需要新增 Entity、Mapper 和 Repository。
- 如果后续要生成记录、OSS 上传、历史查询或重下载,再新增 `workflow_reservation_rooming_list_generation` 表。
- 解析和渲染能力应和 Controller 解耦,便于单元测试。
- 目标 Excel 固定列宽,不调用 POI `autoSizeColumn`,避免后端运行环境依赖本机字体或图形环境。
## 10. 前端实现建议
第一版页面可以放在 Reservation 工具入口,例如:
```text
/reservation/rooming-lists/new
```
页面能力:
- 上传来源 Excel。
- 填写每间房人数。
- 填写目标 `Room Type`
- 目标默认值区域只保留 `Payment Type``Nationality`
- `Payment Type` 使用下拉或固定选择,默认 `CA`,当前不开放其他值。
- `Nationality` 使用下拉,只允许 `KR``CHN`
- 展示解析预览:总人数、预计房间数、前几行分房结果。
- 点击生成后下载 `.xlsx`
前端注意:
- 不要在前端解析完整 Excel 作为权威结果;前端预览可以后置为后端 preview 接口。
- 第一版如果没有 preview 接口,可以只展示用户输入和文件名,生成成功后直接下载。
- CP2 页面不再展示或提交 `arrival``departure``title``rate_code``adults``children``vip``email``id_type``id_number`
- `arrival` / `departure` 由后端从来源 Excel `旅游日期` 解析后写入目标 Excel前端不要自行读取 Excel 并推导日期。
- `Adults` 由后端按分房结果计算;前端不要让用户覆盖。
- 不要把上传文件内容、客人名单或生成文件内容写进浏览器日志、错误上报、URL 或 localStorage。
## 11. 测试范围
后端测试建议覆盖:
- 能识别 `护照全名` 表头。
- 能识别 `旅游日期` 表头,并把 `2026年5月9日5月14日` 解析为 `Arrival=2026-05-09``Departure=2026-05-14`
- `LI/CHUNHONG` 能拆成 `Name=LI``First Name=CHUNHONG`
- `LI CHUNHONG` 能拆成 `Name=LI``First Name=CHUNHONG`
- 姓名首尾空格会 trim。
- `people_per_room=3` 时 32 人生成 11 行。
- 尾房人数不足时仍生成一行。
- `Adults` 按每个房间实际人数派生,尾房人数不足时不使用满房人数。
- `Children` 默认为 `0`
- `payment_type` 为空时按 `CA` 处理,非 `CA` 返回受控错误。
- `nationality=KR` / `CHN` 成功,其他值返回受控错误。
- 来源文件中多个不同旅游日期区间返回受控错误。
- 缺少 `旅游日期` 表头或旅游日期格式无法解析时返回受控错误。
- `Accompanying Guests` 使用英文逗号拼接。
- 未找到表头时返回明确错误。
- 非 Excel 文件、空文件、超大文件返回明确错误。
- 无 token、无权限、无酒店访问权被拦截。
## 12. 后续版本候选
后续可以继续扩展:
- 生成记录表和历史下载。
- 生成文件上传 OSS。
- 后端预览接口。
- 支持前端手工调整分房顺序。
- 支持不同房间不同人数。
- 支持字段映射配置,允许用户选择来源 Excel 中的姓名列。
- 支持从订单、任务或 SourceMessage 附件自动带入名单。
- 支持从任务详情、订单详情或来源附件预填 `Room Type``Payment Type``Nationality`,以及后续经确认恢复的目标字段。