# 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 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-.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`,以及后续经确认恢复的目标字段。