diff --git a/docs/import/20260718/0718给黄哥/00-交付阅读说明.md b/docs/import/20260718/0718给黄哥/00-交付阅读说明.md new file mode 100644 index 0000000..5c59e6d --- /dev/null +++ b/docs/import/20260718/0718给黄哥/00-交付阅读说明.md @@ -0,0 +1,41 @@ +# 0718 预订任务信息系统业务交付包 + +交付日期:2026-07-18 +适用对象:信息系统产品与开发人员 +当前阶段:尚未接入 PMS API + +## 建议阅读顺序 + +1. `01-预订任务信息系统业务需求说明书.md`:主业务需求,开发首先阅读。 +2. `02-业务字段与卡片规则矩阵.md`:按卡片和字段检索具体规则。 +3. `03-业务验收场景清单.md`:用于需求评审、开发自测和业务验收。 +4. `04-前端视觉参考/`:当前页面视觉与场景参考。 + +本交付包的文字材料全部使用 Markdown,不再提供 Word 版本。 + +## 权威性 + +- 主业务需求说明书是本交付包的业务权威文件。 +- 字段矩阵和验收场景用于快速检索与验收,不应创造主文档以外的新规则。 +- 前端截图只说明当前视觉样式和信息组织方式。若截图、当前代码或旧页面行为与主文档冲突,以主文档为准。 +- 历史讨论记录、旧 M002 规格、旧 Agent 输出和旧测试没有放入本交付包,避免开发自行判断新旧规则。 + +## 本交付包回答什么 + +- 系统在业务上要处理什么; +- 一封邮件与订单任务、任务卡如何关联; +- 页面出现哪些卡片及其顺序; +- 每张卡需要哪些业务数据; +- 哪些字段可编辑、如何校验和处理缺失; +- 用户点击“确认”或“终止任务”后发生什么; +- 当前没有 PMS API 时,系统应呈现什么结果; +- 哪些情况属于纯通知、人工复核、系统校验、已终止或异常阻断。 + +## 本交付包不规定什么 + +- 数据库、表结构、接口、DTO、代码模块或技术架构; +- Agent 的识别方法、提示词、训练方法和最终 JSON Schema; +- 尚未取得的 PMS API 请求、失败、重试和回执细节; +- 根级 Invoice 和根级 Rooming List 独立页面的完整业务设计。 + +开发应根据本业务需求自行设计技术方案;遇到业务含义不明确时,应向业务方确认,不应从旧代码或旧文档反推规则。 diff --git a/docs/import/20260718/0718给黄哥/01-预订任务信息系统业务需求说明书.md b/docs/import/20260718/0718给黄哥/01-预订任务信息系统业务需求说明书.md new file mode 100644 index 0000000..8c4e8a4 --- /dev/null +++ b/docs/import/20260718/0718给黄哥/01-预订任务信息系统业务需求说明书.md @@ -0,0 +1,517 @@ +# 预订任务信息系统业务需求说明书 + +版本:V1.0 +日期:2026-07-18 +状态:业务交付基线 +当前运行边界:尚未接入 PMS API + +## 1. 文档目的 + +本说明书用于向信息系统开发人员说明酒店预订任务工作台及任务详情页的业务要求。本文只规定业务目标、业务数据、页面行为、状态和操作结果,不规定数据库、接口或代码实现方式。 + +开发完成后的系统应让酒店预订与运营人员能够: + +- 查看由来源邮件触发的订单任务; +- 核对 Agent 识别出的预订业务参数; +- 按既定权限补正或改选参数; +- 分别确认每张业务任务卡; +- 清楚区分待确认、人工复核、已完成、已终止和异常阻断; +- 在没有 PMS API 时明确知道“信息系统已确认”不等于“PMS 已执行”。 + +## 2. 范围与排除项 + +### 2.1 本次包含 + +- 来源邮件及附件的任务内展示; +- Group 与 Fit 预订任务; +- New Booking、Update Booking、Cancel Booking; +- Basic Information、房间信息、Trace、任务详情内嵌 Rooming List、Payment; +- 纯通知任务; +- 人工复核、查单校验、逐卡确认、确认后锁定; +- 人工终止、异常订单和后续异常阻断; +- 当前无 PMS API 时的信息系统内业务结果。 + +### 2.2 本次不包含 + +- 根级 Invoice 页面。该页面属于独立信息系统,不要求 Agent 提供参数; +- 根级 `/rooming-list` 页面。该页面同样属于独立信息系统; +- Agent 如何识别、提取、训练或调用 Skill; +- 数据库、接口、服务拆分和代码方案; +- 真实 PMS API 尚未提供的请求、失败、重试和回执规则。 + +## 3. 业务参与方与完整链路 + +目标完整链路为: + +`邮件历史线程 → Agent 识别业务与定位参数 → 信息系统查单、展示、校验和接受人工确认 → PMS API 执行 → PMS 返回结果` + +各方业务责任如下: + +- 邮件与历史线程:提供当前业务要求及历史定位信息; +- Agent:判断具体业务类型,给出目标订单及邮件要求的目标参数,并在需要时标记人工复核; +- 信息系统:按目标订单组织任务,查询当前订单,展示和校验数据,接受用户按权限修改并记录最终确认; +- PMS:未来负责真实的新建、更新、取消和备注等酒店业务操作,并返回真实结果。 + +当前没有 PMS API,因此信息系统不得把缺失的 PMS 能力转移给 Agent,也不得伪造 PMS 成功、编号或回执。 + +## 4. 订单任务与任务卡 + +### 4.1 订单任务单位 + +一笔订单任务由“同一来源邮件 + 同一目标订单”确定。 + +- 同一封邮件涉及多个订单时,按目标订单拆成多笔订单任务; +- 同一封邮件中属于同一订单的主预订事件、Trace、Rooming List 和 Payment,聚合到同一个任务详情; +- 订单任务只是聚合容器,真正独立确认和锁定的单位是任务卡。 + +### 4.2 普通预订任务的卡片顺序 + +固定顺序为: + +`Basic Information → 房间信息 → Trace → Rooming List → Payment → 邮件展示` + +- Basic Information、房间信息、邮件展示固定出现; +- Trace、Rooming List、Payment 按 Agent 是否识别出对应业务决定; +- 不存在对应业务时直接跳过,不显示空卡、占位卡或统一“条件卡片”容器; +- Voucher 已并入 Payment,不存在独立 Voucher 卡。 + +### 4.3 纯通知任务 + +没有形成具体预订业务任务时,页面只显示邮件展示卡: + +- 不显示 Basic Information、房间信息或其他业务卡; +- 邮件展示卡是唯一可确认任务卡; +- 用户点击“确认”后任务完成; +- 仅打开页面不会自动完成; +- 不提供人工终止,不调用 PMS,也不修改订单。 + +## 5. 通用任务卡规则 + +### 5.1 独立确认 + +Basic Information、房间信息,以及实际出现的 Trace、Rooming List、Payment,分别是独立任务卡。 + +- 每张卡有自己的“确认”按钮; +- 不存在订单任务级的全局确认按钮; +- 点击某张卡的“确认”只提交、执行并锁定该卡; +- 一张卡的问题不阻塞同一订单任务中的其他合法卡片。 + +普通预订中的邮件展示卡只是来源证据,不显示“确认”;纯通知中的邮件展示卡例外,是唯一可确认卡。 + +### 5.2 确认前编辑与草稿 + +- 用户只能按各字段既定权限编辑;人工复核不等于所有字段都能自由输入; +- 未确认修改只保留在当前页面,离开后无需保存; +- 不自动保存,也不提供“保存草稿”; +- 信息系统长期保留 Agent 原始值与用户最终确认值,不保存每一次中间编辑历史。 + +### 5.3 确认后锁定 + +- 每张卡只能确认一次; +- 确认后卡片永久锁定,不能再次编辑、确认或重新执行; +- 信息系统不能从已完成卡片复制或创建新业务任务; +- 新任务只能由新的邮件或其他程序输入再次触发 Agent 后产生; +- 执行后发现错误,不能修改旧任务伪装成回滚。未来可直接在 PMS 中纠正,后续新任务再读取 PMS 最新数据。 + +### 5.4 卡片状态 + +任务卡业务状态包括: + +- 待确认; +- 待确认 + 需人工复核; +- 已完成; +- 已终止。 + +“需人工复核”是待确认卡上的附加标记,不是每张卡都必经的独立步骤。 + +### 5.5 订单任务汇总状态 + +- 所有可执行卡均未完成:待处理; +- 至少一张完成且仍有卡未完成:处理中; +- 所有可执行卡均完成:已完成; +- 任一卡触发订单级终止:已终止; +- 异常订单后续新任务被阻断:异常阻断。 + +订单任务不显示“已完成 2/5”等进度计数。单卡的“需人工复核”不上卷为订单级状态或额外提示。普通预订邮件证据卡不参与状态汇总。 + +## 6. 人工复核、系统查单校验与卡型错误 + +### 6.1 人工复核 + +人工复核由 Agent 针对已经确定的具体业务卡给出。 + +- 卡片仍使用原业务类型,不生成独立人工复核卡; +- 页面显示“待确认 + 需人工复核”; +- 用户查看原邮件,按该卡原有字段权限补正或核对; +- 数据满足校验后,直接点击同一个“确认”完成; +- 不设置“确认修改”“解除复核”或二次确认步骤; +- 无法确定具体业务类型时走纯通知,不生成 Fallback 或 Need Manual Review 卡。 + +### 6.2 已有订单的唯一匹配门槛 + +Update、Cancel、Trace、Rooming List、Payment 等已有订单业务,必须先唯一匹配订单。 + +- Group 使用 Group Code,即 Block Name; +- Fit 使用 Confirmation Number; +- 当前无 PMS API 时查询信息系统已有订单;未来有 API 时查询 PMS; +- 查询到 0 笔或多笔都不允许系统自动选择; +- 匹配前仍显示完整业务卡,但只有定位字段可编辑,其余字段可见但禁用,“确认”不可用; +- 统一提示:“未唯一匹配到订单,请核对 Group Code / Confirmation Number”; +- 如果 Agent 没有要求人工复核,系统查单失败仍保持“待确认”,不能自行追加“需人工复核”。 + +### 6.3 卡型错误 + +用户发现 Agent 漏识别、误识别或生成错误卡型时: + +- 信息系统不自动重跑 Agent; +- 用户不能在详情页自行新增、删除或切换业务卡; +- 用户从任一未完成预订业务卡触发“终止任务”; +- 纯通知任务不存在人工终止。 + +## 7. Basic Information 卡 + +字段固定为:`Account → Market → Source`。 + +- 三项都只能从信息系统已有目录中选择,不允许自由输入; +- Account 没有 Manual 选项; +- 系统根据 Account 目录关系自动带出 Market 与 Source; +- 用户仍可在已有目录中分别改选 Account、Market、Source; +- 没有可靠 Account 或目录无法匹配时,该卡进入人工复核,用户从已有目录补选; +- 如果系统目录本身缺少选项,应走主数据维护,不能在任务卡临时输入新值。 + +当前无 PMS API 时,点击“确认”只保存最终 Account、Market、Source,卡片变为已完成并锁定,不产生任何 PMS 成功语义。 + +## 8. 房间信息卡的公共业务结构 + +### 8.1 Group 与 Fit + +Group: + +- 卡头显示 Group; +- 对象标识为 Block Name; +- `Block Name = Group Code`,它是邮件中的团号,不是住客姓名; +- 显示 Group Booking Status,受控值为 TEN、DEF、INQ。 + +Fit: + +- 卡头显示 Fit; +- 对象标识为 Name; +- 不再使用 Con. No. 或 `Block Name/Con. No.` 组合名称。 + +### 8.2 订单级字段 + +- Block Name 或 Name; +- 入住日期; +- 离店日期; +- Nights; +- 订单级 Rate Code,只显示一次; +- Breakfast; +- Group 额外显示 Group Booking Status。 + +### 8.3 房型行 + +每行固定为:`RoomType → 房量 → Adult(每间)`。 + +- 允许多行房型; +- RoomType 只能从信息系统已有房型选择; +- 房量为正数,用户可修改; +- Adult 表示每间成人数,不是该行成人总数; +- Adult 由 RoomType 的系统映射产生,普通房间信息卡中不允许直接修改; +- 同一 RoomType 只有 Adult 不同时才允许拆成多行; +- Rate Code 不放在房型行内,也不按 Rate Code 拆分房型。 + +### 8.4 字段规则 + +- 入住与离店日期可由用户纠错;任一缺失进入人工复核; +- Nights 由系统根据日期计算,不由用户覆盖; +- Rate Code 必填、无默认值,只能从系统已有值选择; +- Group 固定包含早餐;Fit 的 Breakfast 由 Rate Code 规则派生,不能直接修改; +- 普通 Group New 初始状态为 TEN;Proposal 为 INQ;Group 的 Rooming List 或 Payment 确认后处理为 DEF; +- 用户可在卡片确认前从 TEN、DEF、INQ 中受控改选 Group Booking Status。 + +### 8.5 Fit Name 生命周期 + +- 首封邮件已有真实住客姓名:直接使用真实姓名创建; +- 首封邮件没有真实姓名但有 Booking Code:以 Booking Code 临时作为 Name,信息完整,不触发人工复核; +- 后续邮件提供真实姓名:通过正式 Update Booking 更新同一订单的 Name; +- 更新为真实姓名后,Booking Code 不再作为订单关联键或 PMS Name; +- 已有 Fit 订单后续操作使用 Confirmation Number 定位,不使用 Name。 + +### 8.6 PMS 创建结果 + +未来 PMS New Booking 成功后: + +- Group 返回 Block ID; +- Fit 返回 Confirmation Number; +- 两项均在房间信息卡和执行结果/订单信息区域显示; +- 两项只读,不允许用户修改; +- 当前无 PMS API 时不得生成模拟值,显示“未接入 PMS API”。 + +## 9. New Booking + +- 展示完整目标预订信息; +- 用户按各字段权限在确认前纠错; +- Group Code 同时作为 Block Name; +- Fit 没有真实姓名时使用 Booking Code 作为初始 Name; +- Group 普通预订初始 TEN,Proposal 初始 INQ; +- 确认后原 New Booking 卡永久锁定; +- 创建后的任何变化都属于新的 Update Booking,不能重新打开原 New Booking。 + +当前无 PMS API时: + +- 保存用户最终确认的完整目标预订参数; +- 房间信息卡变为已完成并锁定; +- 结果明确显示“信息系统已确认,未调用 PMS”; +- 不显示“创建成功”,不生成 Block ID 或 Confirmation Number。 + +## 10. Update Booking + +### 10.1 定位与当前订单 + +- Group 一律使用 Group Code / Block Name 定位,不优先使用 Block ID; +- Fit 一律使用 Confirmation Number 定位,不使用 Name; +- 当前订单参数由信息系统查询取得,Agent 不提供原参数。 + +### 10.2 页面展示 + +- 显示修改后的完整最新订单; +- 对发生变化的字段突出显示“原参数 → 修改后参数”; +- 日期变化同时显示系统重新计算的 Nights; +- 房型或房量变化时,分别展示修改前完整房型清单和修改后完整房型清单; +- 不追踪某一原房型行如何新增、删除、拆分或转换; +- Rate Code 在建单时确定,不属于 Update 可修改字段; +- Fit Name、日期、RoomType 和房量可作为 Update 内容。 + +### 10.3 当前无 PMS API + +- 确认后保存最终修改后参数; +- 把最终变化应用到信息系统内的本地订单最新数据,供后续无 API 任务查询; +- 卡片变为已完成并锁定; +- 结果显示“信息系统已确认,未调用 PMS”; +- 不生成新的 PMS 标识或 PMS 更新回执。 + +## 11. Cancel Booking + +- Group 与 Fit 都只允许整单取消; +- 页面先调出并只读展示完整当前订单; +- 不存在修改后参数,只表达“将取消整笔订单”; +- 减少房量、删除房型、修改日期或修改 Name 都属于 Update,不属于 Cancel。 + +当前无 PMS API时: + +- 保存整单取消意图、目标订单和确认时展示的原订单参数; +- 保留订单及所有历史,将本地订单标记为已取消; +- 卡片变为已完成并锁定; +- 结果显示“信息系统已确认,未调用 PMS”; +- 不删除订单、不清除 PMS 标识,也不伪造 PMS 已取消; +- 后续仍可定位该记录,但新的 Update、Trace、Rooming List、Payment 显示“订单已取消”并禁止确认。 + +## 12. Trace 卡 + +### 12.1 出现与数量 + +- New Booking 和 Update Booking 可以带 Trace;Cancel Booking 不会带 Trace; +- 未识别出 Trace 时不显示; +- 同一订单只显示一张 Trace 卡,卡内可以包含多条事项; +- Trace 关联整笔订单,不默认拆成日期、房间或住客级任务; +- Trace 与 Rooming List 没有业务前后置关系。 + +### 12.2 普通备注 + +每条普通备注包含: + +- 事项内容:由 Agent 根据邮件提炼,确认前允许用户修改; +- Department:只能选择信息系统预设部门或组合部门,确认前允许改选。 + +没有可识别事项内容就不存在该 Trace item,不创建空事项。 + +### 12.3 加床备注 + +加床备注默认事项内容为 `SET EXTRA BED`,并显示: + +- 目标 RoomType; +- 加床房间数量; +- 加床后 Adult(每间); +- Department。 + +业务规则: + +- 目标 RoomType 只能从当前预订已有房型中选择; +- 加床房间数量允许用户修改;邮件只写单数“加床”且无其他数量时,默认 1 间; +- 加床后 Adult 由系统按当前 Adult +1 产生初始值,用户可在 Trace 卡确认前修改; +- 页面把三项相邻回显,例如 `TWN × 1|Adult 3`; +- Adult 表示加床后的每间成人数,不表示加床房间数量; +- Department 应由 Agent提供受控值;如果运行异常导致缺失,信息系统以 `FO+HSK` 兜底并允许用户改选; +- 加床可以与蜜月布置等普通备注同时存在; +- 目标房型缺失或不属于当前订单,说明订单关联或解析错误,整笔订单需要人工核对。 + +### 12.4 当前无 PMS API + +- 点击“确认”保存卡内全部最终事项和确认时间; +- 卡片变为已完成并锁定; +- 结果显示“信息系统已确认,未调用 PMS”; +- 不宣称 Department 已同步、Trace 已写入 PMS或加床已生效; +- 不改写房间信息卡或订单房间汇总。已锁定 Trace 中的 `TWN × 1|Adult 3`只表示已确认的目标参数。 + +## 13. 任务详情内嵌 Rooming List + +### 13.1 业务范围 + +- 只有 Agent 识别出 Rooming List 时才显示; +- 只适用于 Group,Fit 不会有 Rooming List; +- 一封邮件涉及多个订单时按订单拆任务; +- 同一 Group 后续收到新版 Rooming List 时,为新邮件创建新任务并关联原订单,旧任务与旧邮件保留; +- 使用 Group Code / Block Name 查询原 Group 订单,不优先使用 Block ID; +- 原始附件只在邮件展示卡查看,不在 Rooming List 卡重复展示。 + +### 13.2 当前简化展示 + +当前尚无 PMS API,本阶段只要求识别任务并展示类似标准表格的卡片。表头显示以下 12 个业务字段: + +`Name → First Name → Arrival → Departure → Room Type → Rate Code → Number of Rooms → Adults → Children → Payment Type → Accompanying Guests → Nationality` + +- Line 如显示,只是前端辅助序号; +- 表格内容可以使用演示数据,不要求证明已经准确转换原附件; +- 当前不要求真实逐位住客解析、同住分组、复杂编辑、分页、固定列、Excel 下载或手工导入 PMS; +- 用户点击“确认”后卡片完成并锁定; +- 当前确认只表示信息系统已确认,未调用 PMS; +- Group Booking Status 按业务规则处理为 DEF; +- 未来取得 PMS API 后,重新确认真实住客、字段、校验和执行参数,直接通过 API,不经过 Excel。 + +## 14. Payment 卡 + +### 14.1 出现与展示 + +- Payment 是否存在由 Agent 识别,信息系统不重新判断“什么算付款”; +- Payment 只属于 Update Booking; +- Group 与 Fit 都可能出现; +- 一封邮件涉及多个订单时按订单拆任务; +- 同一订单的多份付款凭证在一张 Payment 卡中全部展示,不按附件拆卡; +- 凭证之间没有业务顺序; +- Voucher 与 Payment Evidence 都统一为 Payment,不存在独立 Voucher 卡。 + +### 14.2 当前所需业务数据 + +- Payment 事件; +- 目标订单; +- 与当前 Payment 对应的一份或多份来源附件。 + +当前不需要金额、付款日期、付款人、交易号、银行账号、QR、备注、付款状态或 Department。凭证存在不等于酒店已确认到账。 + +### 14.3 当前无 PMS API + +- 用户先查看凭证,再点击“确认”; +- Group:本地 Group Booking Status 更新为 DEF,Payment 卡完成并锁定; +- Fit:Payment 卡直接完成并锁定,不新增其他订单字段; +- 不显示 Payment Confirmed,不代表到账确认或 PMS 成功; +- 当前不设计 PMS 失败、重试或回执,也没有独立“重试”按钮。 + +## 15. 邮件展示卡 + +邮件展示卡只展开触发当前任务的这一封来源邮件,不在卡内展开整条历史线程。 + +固定显示: + +- 邮件主题; +- 发件人; +- 发送时间; +- 邮件原文; +- 当前邮件原始附件。 + +同时提供“查看完整邮件”入口,用于打开对应邮件会话。 + +权限规则: + +- 主题、发件人、时间和正文只读; +- 附件允许预览或下载,不允许新增、删除或替换; +- 业务卡内的修改不能反向改写来源邮件; +- 普通预订中邮件卡只是证据;纯通知中邮件卡是唯一可确认任务卡。 + +附件业务要求: + +- 每个附件至少具备稳定标识、文件名、内容类型和可访问地址;文件大小可选; +- 访问地址由邮件监听或文件存储提供,Agent 只能原样传递; +- Payment 只引用邮件附件,不复制第二份完整附件对象。 + +## 16. 人工终止、异常订单与后续阻断 + +### 16.1 人工终止 + +- 每张尚未完成、未被异常阻断的预订业务卡都可触发“终止任务”; +- 操作作用于整笔订单任务,不是只终止当前卡; +- 所有未完成卡变为已终止并锁定; +- 已完成卡继续保持已完成,保留其真实参数、确认时间和业务结果; +- 订单任务整体变为已终止; +- 终止不调用 PMS,也不回滚已完成卡; +- 纯通知任务没有“终止任务”。 + +### 16.2 异常订单 + +人工终止后,关联订单标记为异常订单: + +- 异常标识说明 Agent 结果或订单上下文不可信; +- 酒店用户不负责维护或解除异常标识; +- 后续新任务仍可查看邮件和卡片内容,但状态为“异常阻断”; +- 不显示可用确认入口,不允许调用 PMS; +- 异常问题交由开发或 Agent 维护人员调查。 + +### 16.3 终止记录 + +- 固定原因:人工终止; +- 记录终止时间; +- 不要求自由文本原因; +- 当前没有账户架构,不记录或伪造终止人; +- 原邮件和 Agent 结果复用任务已有引用,不重复保存副本。 + +## 17. 纯通知与特殊结果 + +以下情况统一采用纯通知页面: + +- 内容可理解但不属于预订部自动处理; +- 无法可靠判断具体任务类型; +- 材料不足或附件不可用,无法形成具体业务事件; +- 极少数 AMEND GROUP CODE 业务,由用户在 PMS 外部人工处理。 + +页面只显示邮件展示卡。用户完成必要的外部处理后点击“确认”,任务完成并锁定;不显示业务字段、人工复核卡或人工终止。 + +不再存在 S99、独立 Need Manual Review、Fallback、未处理意图卡或独立 Voucher 卡。 + +信息系统收到无法解析的 Agent 技术输出属于技术异常:不创建酒店用户可见任务或卡片,由开发处理。该技术异常不能伪装成纯通知或人工复核。 + +## 18. 记录与审计要求 + +每张可执行卡独立保存: + +- Agent 原始业务值; +- 用户最终确认值; +- 卡片状态; +- 确认时间; +- 当前阶段适用的业务结果; +- 锁定状态。 + +当前不记录确认人。终止记录只保存固定原因和终止时间。不得保存未确认草稿或逐次中间编辑历史。 + +## 19. 当前无 PMS API 的统一原则 + +- “已完成”表示该任务卡已经在信息系统内完成确认和当前阶段要求的本地处理; +- “已完成”不等于 PMS 已成功执行; +- 不伪造 Block ID、Confirmation Number、PMS 成功回执、部门已同步、付款已确认或加床已生效; +- New、Update、Cancel、Trace 的结果明确显示“信息系统已确认,未调用 PMS”; +- Update 更新本地订单最新数据,Cancel 标记本地已取消,Group Payment / Rooming List 可按业务规则更新本地 Group Booking Status; +- 上述本地变化只服务当前演示和后续本地查单,不宣称 PMS 已同步; +- PMS 的请求、失败、重试、回执和再次提交规则等取得真实 API 后再确认。 + +## 20. 业务验收与后续事项 + +开发应使用 `03-业务验收场景清单.md` 完成需求评审和业务验收,并使用 `02-业务字段与卡片规则矩阵.md` 检查字段权限与确认结果。 + +以下事项明确后置,不阻塞当前开发: + +- 真实 PMS API 参数和执行结果; +- Rooming List 的真实住客解析、同住分组和 PMS API 数据结构; +- 账户体系建立后的确认人、终止人; +- 无附件与附件监听/读取失败的精确技术表达; +- Agent 最终 JSON Schema、训练案例和 Skill 调试; +- 根级 Invoice 与根级 Rooming List 的独立业务说明。 + +开发不得使用后置事项自行扩展当前业务范围,也不得从旧代码、旧 M002 文档或历史讨论恢复已经废止的规则。 diff --git a/docs/import/20260718/0718给黄哥/02-业务字段与卡片规则矩阵.md b/docs/import/20260718/0718给黄哥/02-业务字段与卡片规则矩阵.md new file mode 100644 index 0000000..b34abbb --- /dev/null +++ b/docs/import/20260718/0718给黄哥/02-业务字段与卡片规则矩阵.md @@ -0,0 +1,332 @@ +# 预订任务信息系统业务字段与卡片规则矩阵 + +> 版本日期:2026-07-18 +> 用途:帮助产品、设计、开发和测试快速核对每张任务卡需要什么业务数据、如何展示、哪些内容可编辑,以及确认后的业务结果。 +> 本文不规定数据库表、接口结构或代码实现。若与其他材料冲突,以 `01-预订任务信息系统业务需求说明书.md` 为准。 + +## 一、任务与卡片总览 + +### 1. 普通预订任务 + +固定展示顺序: + +`Basic Information → 房间信息 → Trace → Rooming List → Payment → 邮件展示` + +| 卡片 | 出现条件 | 是否独立确认 | 普通状态 | 确认后 | 是否可触发终止 | +|---|---|---|---|---|---| +| Basic Information | 普通预订任务固定出现 | 是 | 待确认 / 待确认 + 需人工复核 / 已完成 / 已终止 | 保存最终选择并锁定 | 是,未完成时可触发整笔订单任务终止 | +| 房间信息 | 普通预订任务固定出现 | 是 | 同上 | 按 New / Update / Cancel 规则完成并锁定 | 是 | +| Trace | Agent 给出 Trace 业务时出现 | 是 | 同上 | 保存全部 Trace 最终参数并锁定 | 是 | +| Rooming List | Agent 给出 Group Rooming List 业务时出现 | 是 | 同上 | 当前完成展示确认并锁定 | 是 | +| Payment | Agent 给出 Payment 业务时出现 | 是 | 同上 | 完成凭证确认及当前本地处理并锁定 | 是 | +| 邮件展示 | 普通预订任务固定出现 | 否,只作来源证据 | 不参与订单任务进度 | 无单独确认动作 | 否 | + +### 2. 纯通知任务 + +| 项目 | 规则 | +|---|---| +| 页面内容 | 只显示邮件展示卡,不显示任何预订业务卡 | +| 完成方式 | 用户点击邮件展示卡中的“确认”后完成;仅打开页面不自动完成 | +| 人工复核 | 不存在 | +| 人工终止 | 不存在 | +| 订单修改 | 不查询或修改预订订单 | +| PMS | 不调用 PMS | + +### 3. 通用确认规则 + +| 规则项 | 业务规则 | +|---|---| +| 确认单位 | 每张任务卡独立确认;不存在订单任务级全局确认 | +| 卡片互不阻塞 | 单张卡的问题不阻塞同订单任务中的其他合法卡片 | +| 草稿 | 不自动保存,不提供保存草稿;离开页面后未确认修改无需保留 | +| 长期记录 | 保留 Agent 原始值和用户最终确认值,不保留每次中间编辑历史 | +| 确认次数 | 每张卡只能确认一次 | +| 确认后 | 永久锁定,不能再次编辑、确认或重新执行 | +| 错误纠正 | 不重开旧任务;后续变更必须由新的程序输入经 Agent 生成新任务 | + +## 二、Basic Information 卡 + +字段顺序固定为:`Account → Market → Source`。 + +| 字段 | 业务含义与来源 | 是否必需 | 展示与编辑 | 缺失或无效时 | 当前无 PMS API 的确认结果 | +|---|---|---|---|---|---| +| Account | 由邮件发件人等业务信息确定,并匹配信息系统已有 Account 目录 | 是 | 受控选择;用户可从已有值改选;不可自由输入;没有 Manual | 卡片显示需人工复核,用户从已有目录补选 | 保存最终选择 | +| Market | 与 Account 存在系统预设关系,由系统自动带出 | 是 | 受控选择;用户可从已有值改选;不可自由输入 | 同上 | 保存最终选择 | +| Source | 与 Account 存在系统预设关系,由系统自动带出 | 是 | 受控选择;用户可从已有值改选;不可自由输入 | 同上 | 保存最终选择 | + +补充规则:如果目录本身缺少可选值,应进入主数据维护,不允许在任务卡临时造出系统不存在的值。 + +## 三、房间信息卡——公共字段 + +### 1. 订单对象与订单级字段 + +| 字段 | 适用范围 | 业务来源或派生规则 | 是否必需 | 确认前权限 | 校验与特殊规则 | +|---|---|---|---|---|---| +| 预订类型标签 | Group / Fit | 业务事件确定 | 是 | 不可改 | Group 与 Fit 使用不同定位和对象名称 | +| Block Name | Group | 等于邮件中的 Group Code,是团号,不是住客姓名 | 是 | 创建前可纠错;确认后锁定 | 已有 Group 订单后续仍以 Group Code / Block Name 定位,不优先用 Block ID | +| Name | Fit | 优先真实住客姓名;暂无真实姓名时可临时使用 Booking Code | 是 | 创建前可纠错;确认后锁定 | 后续真实姓名通过 Update 修改;已有订单后续定位使用 Confirmation Number,不使用 Name | +| 入住日期 | Group / Fit | 目标预订信息 | 是 | 可改 | 缺失时需人工复核 | +| 离店日期 | Group / Fit | 目标预订信息 | 是 | 可改 | 缺失时需人工复核;必须晚于入住日期 | +| Nights | Group / Fit | 系统按入住、离店日期自动计算 | 是,系统派生 | 只读 | 不要求 Agent 提供,不能由用户覆盖 | +| Rate Code | Group / Fit | 从业务邮件取得并匹配系统已有 Rate Code | 是 | New 创建前可从已有值改选;Update 不允许修改 | 订单级字段,只显示一次;无默认值;不能自由输入 | +| Breakfast | Group / Fit | Group 固定包含;Fit 由 Rate Code 规则派生 | 是,系统派生 | 只读 | 需要变更时应修改合法 Rate Code,而不是直接改 Breakfast | +| Group Booking Status | 仅 Group | 普通 New 为 TEN;Proposal 为 INQ;Rooming List 或 Payment 确认后为 DEF | 是,系统有初始规则 | 确认前可在 TEN / DEF / INQ 中受控改选 | 不要求 Agent 输出;Fit 不显示 | +| Block ID | 仅 Group,PMS New 成功后 | PMS 返回 | 当前无 API 时不存在 | 只读 | 在房间信息卡和执行结果/订单信息区域显示;当前不得模拟 | +| Confirmation Number | 仅 Fit,PMS New 成功后 | PMS 返回 | 当前无 API 时不存在 | 只读 | 在房间信息卡和执行结果/订单信息区域显示;已有 Fit 后续业务靠它定位 | + +### 2. 房型行 + +每行固定为:`RoomType → 房量 → Adult(每间)`。 + +| 字段 | 业务来源或派生规则 | 是否必需 | 确认前权限 | 校验与特殊规则 | +|---|---|---|---|---| +| RoomType | 目标预订中的房型,匹配系统已有房型目录 | 是 | 只能从已有房型中改选 | 缺失或无法匹配时需人工复核 | +| 房量 | 目标预订中该房型的间数 | 是 | 可修改正整数 | 缺失、为 0 或无法确定时需人工复核 | +| Adult(每间) | 系统按 RoomType 的固定入住人数映射产生 | 是,系统派生 | 普通房间信息卡中只读 | 表示每间成人数,不是该行总人数;不存在无映射房型 | + +房型行补充规则: + +- 允许多个房型行; +- Rate Code 不属于房型行,不按 Rate Code 拆行; +- 同一 RoomType 只有 Adult 不同时才允许拆成多行,例如 `TWN × 4|Adult 2` 与 `TWN × 1|Adult 3`。 + +## 四、房间信息卡——New / Update / Cancel + +| 类型 | 订单定位 | 页面主要内容 | 确认前权限 | 当前无 PMS API 的确认结果 | +|---|---|---|---|---| +| New Booking | Group 用 Group Code 建单;Fit 用真实姓名,暂无姓名时用 Booking Code 临时作为 Name | 展示完整目标预订 | 按公共字段权限纠错 | 保存完整目标参数;卡片完成并锁定;显示“信息系统已确认,未调用 PMS”;不显示创建成功,不生成 PMS 编号 | +| Update Booking | Group 用 Group Code / Block Name;Fit 用 Confirmation Number | 展示修改后的完整订单,并突出变化字段的“原参数 → 修改后参数” | 修改后参数按字段权限纠错;Rate Code 不可修改 | 保存最终修改并更新信息系统本地订单最新数据;完成并锁定;不伪造 PMS 回执 | +| Cancel Booking | Group 用 Group Code / Block Name;Fit 用 Confirmation Number | 只读展示完整当前订单,并表达“将取消整笔订单” | 当前订单参数不可改;只能确认或终止任务 | 保留订单历史并把本地订单标记为已取消;完成并锁定;不删除订单、不清除 PMS 标识、不伪造 PMS 已取消 | + +### Update 变化展示 + +| 变化类型 | 页面突出方式 | +|---|---| +| 日期变化 | 显示原入住/离店日期 → 修改后日期,并显示系统重算后的 Nights | +| Name 变化 | 显示原 Name → 修改后 Name | +| 房型或房量变化 | 分别显示修改前完整房型清单和修改后完整房型清单 | +| Rate Code | 不属于 Update 可修改内容,不展示为可变字段 | + +系统无需追踪“某一原房型行变成哪一新行”;只需准确展示完整 Before 清单和完整 After 清单。 + +## 五、已有订单的定位门槛 + +适用于 Update、Cancel、Trace、Rooming List 和 Payment。 + +| 场景 | 页面行为 | 可编辑范围 | 确认按钮 | +|---|---|---|---| +| 唯一匹配 1 笔订单 | 回显卡片所需订单数据 | 按卡片字段权限 | 满足卡片校验后可用 | +| 匹配 0 笔 | 完整卡片仍显示,但未取得订单数据的部分禁用 | 只允许修改 Group Code / Confirmation Number | 禁用 | +| 匹配多笔 | 同上,系统不能自行选择 | 只允许修改定位字段 | 禁用 | +| 定位后唯一匹配 | 重新回显订单和卡片参数 | 恢复正常字段权限 | 满足校验后可用 | + +统一提示:`未唯一匹配到订单,请核对 Group Code / Confirmation Number`。 + +当前无 PMS API 时查信息系统本地订单;未来有 PMS API 时改为查询 PMS。系统查单失败不会擅自给卡片添加“需人工复核”标记。 + +## 六、Trace 卡 + +### 1. 卡片级规则 + +| 项目 | 规则 | +|---|---| +| 出现范围 | New Booking、Update Booking 可出现;Cancel Booking 不出现 | +| 数量 | 同一订单只显示一张 Trace 卡,卡内可有多条事项 | +| 关联范围 | 关联整笔订单 | +| 与 Rooming List 的关系 | 没有前后置或依赖关系 | +| 确认单位 | 整张 Trace 卡一次确认,提交卡内全部事项 | + +### 2. 普通备注事项 + +| 字段 | 业务规则 | 是否必需 | 确认前权限 | 缺失处理 | +|---|---|---|---|---| +| 事项内容 | 对邮件要求的业务事项进行提炼 | 是 | 可修改 | 没有事项内容就不应创建该 Trace item | +| Department | 负责接收该事项的系统预设部门或组合部门 | 是 | 受控选择,可改选 | Agent 应提供;异常缺失时系统兜底 FO+HSK | + +### 3. 加床事项 + +| 字段 | 业务规则 | 是否必需 | 确认前权限 | 校验与展示 | +|---|---|---|---|---| +| 事项内容 | 系统固定生成 `SET EXTRA BED` | 是 | 可见;业务文案固定 | 可与蜜月布置等普通事项并存 | +| 目标 RoomType | 当前订单中需要加床的房型 | 是 | 只能从当前订单已有房型中改选 | 缺失或不属于当前订单说明关联或解析错误,需要核对整笔订单 | +| 加床房间数量 | 该房型中需要加床的间数 | 是 | 可修改正整数 | 邮件只写单数“加床”且未给数量时,默认为 1 | +| 加床后 Adult(每间) | 系统初始按该房型当前 Adult +1 计算 | 是 | 可修改 | 表示加床后每间成人数,不表示加床房间数 | +| Department | 负责部门或组合部门 | 是 | 受控选择,可改选 | 异常缺失时系统兜底 FO+HSK | + +页面应相邻回显加床目标,例如:`TWN × 1|Adult 3`。 + +### 4. Trace 确认结果 + +当前无 PMS API 时: + +- 保存卡内全部最终事项和确认时间; +- 卡片变为已完成并锁定; +- 显示“信息系统已确认,未调用 PMS”; +- 不宣称 Department 已同步、Trace 已写入 PMS 或加床已生效; +- 不改写房间信息卡或订单房间汇总。 + +## 七、任务详情内嵌 Rooming List 卡 + +### 1. 出现与任务关系 + +| 项目 | 规则 | +|---|---| +| 适用订单 | 仅 Group;Fit 不会出现 | +| 出现条件 | Agent 给出 Rooming List 业务 | +| 同邮件多订单 | 按目标订单拆成多笔订单任务 | +| 同订单新版名单 | 新邮件形成新任务,关联同一 Group;旧任务、旧邮件和旧结果保留 | +| 定位 | Group Code / Block Name,不优先用 Block ID | +| 原始附件 | 只在邮件展示卡查看,不在 Rooming List 卡重复展示 | + +### 2. 当前标准表格字段 + +当前仅展示以下必填业务列;Line 若出现,只是辅助序号。 + +| 顺序 | 字段 | 当前页面要求 | +|---:|---|---| +| 1 | Name | 显示为标准表格列 | +| 2 | First Name | 显示为标准表格列 | +| 3 | Arrival | 显示为标准表格列 | +| 4 | Departure | 显示为标准表格列 | +| 5 | Room Type | 显示为标准表格列 | +| 6 | Rate Code | 显示为标准表格列 | +| 7 | Number of Rooms | 显示为标准表格列 | +| 8 | Adults | 显示为标准表格列 | +| 9 | Children | 显示为标准表格列 | +| 10 | Payment Type | 显示为标准表格列 | +| 11 | Accompanying Guests | 显示为标准表格列 | +| 12 | Nationality | 显示为标准表格列 | + +### 3. 当前范围与确认结果 + +| 项目 | 当前要求 | +|---|---| +| 表格内容 | 可使用演示数据,不要求证明已准确转换原附件 | +| 真实解析 | 暂不要求真实逐位住客解析、同住分组或复杂校验 | +| 交互 | 暂不要求复杂编辑、分页或固定列 | +| Excel | 不生成、不下载、不要求用户手工导入 PMS | +| 确认 | 用户点击确认后卡片完成并锁定 | +| Group 状态 | 按业务规则把本地 Group Booking Status 处理为 DEF | +| PMS 语义 | 只表示信息系统确认,未调用 PMS | + +未来拿到 PMS API 后,重新确认真实住客结构、必填校验和执行参数,直接调用 API,不经过 Excel。 + +## 八、Payment 卡 + +| 项目 | 业务规则 | +|---|---| +| 适用事件 | 仅属于 Update Booking | +| 适用订单 | Group、Fit 均可 | +| 同邮件多订单 | 按订单拆任务 | +| 同订单多凭证 | 一张 Payment 卡展示全部凭证,不按附件拆卡 | +| 凭证顺序 | 没有业务顺序 | +| Voucher | 已并入 Payment,不存在独立 Voucher 卡 | +| 页面所需数据 | Payment 事件、目标订单、一份或多份对应来源附件 | +| 当前不需要 | 金额、付款日期、付款人、交易号、银行账号、QR、备注、付款状态、Department | +| 业务含义 | 展示凭证不等于酒店已确认到账 | + +### Payment 确认结果 + +| 订单类型 | 当前无 PMS API 的结果 | +|---|---| +| Group | 用户查看凭证并确认;本地 Group Booking Status 更新为 DEF;Payment 卡完成并锁定 | +| Fit | 用户查看凭证并确认;Payment 卡直接完成并锁定,不新增其他订单字段 | + +两种情况都不得显示 `Payment Confirmed`,不得代表到账确认或 PMS 成功;当前不设计重试按钮或 PMS 回执。 + +## 九、邮件展示卡 + +| 字段或功能 | 是否固定显示 | 权限与规则 | +|---|---|---| +| 邮件主题 | 是 | 只读;监听值缺失时可为 null | +| 发件人 | 是 | 只读;使用邮件监听得到的发件人值 | +| 发送时间 | 是 | 只读 | +| 邮件原文 | 是 | 只读;只展开触发当前任务的这一封邮件 | +| 当前邮件附件 | 是 | 可预览或下载;不可新增、删除或替换 | +| 查看完整邮件 | 是 | 打开对应完整邮件会话,不在卡内直接铺开整个历史线程 | + +附件至少需要满足以下业务展示条件: + +| 附件信息 | 要求 | +|---|---| +| 稳定标识 | 必需,用于区分并关联附件 | +| 文件名 | 必需 | +| 内容类型 | 必需,用于决定预览方式 | +| 可访问地址 | 必需,由邮件监听或文件存储提供 | +| 文件大小 | 可选 | + +Payment 只引用邮件展示卡对应的来源附件,不需要再复制一份附件内容。 + +## 十、人工复核、校验与卡型错误 + +| 情况 | 页面状态 | 用户处理 | 是否影响其他卡 | +|---|---|---|---| +| 具体业务已确定,但该卡数据需核对或补正 | 待确认 + 需人工复核 | 按原字段权限补正后直接点击同一个“确认” | 不影响其他合法卡 | +| 必填校验未通过 | 保持待确认状态并显示字段问题 | 修正合法字段 | 不影响其他合法卡 | +| 已有订单未唯一匹配 | 待确认;非定位字段全部禁用 | 先补正 Group Code / Confirmation Number | 不影响其他合法卡 | +| Agent 漏识别、误识别或生成错误卡型 | 信息系统不新增、删除或切换卡型 | 用户从任一未完成预订业务卡触发终止 | 终止作用于整笔订单任务 | +| 无法确定具体任务类型 | 纯通知 | 查看邮件并确认 | 不生成业务卡或人工复核卡 | +| Agent 输出结构无法解析 | 技术异常 | 由开发处理 | 不创建酒店用户可见任务 | + +## 十一、终止与异常阻断 + +| 对象或场景 | 结果 | +|---|---| +| 触发入口 | 任一尚未完成、未被阻断的预订业务卡上的“终止任务” | +| 已完成卡 | 保持已完成,保留真实参数、确认时间和结果 | +| 未完成卡 | 全部变为已终止并锁定 | +| 订单任务 | 整体变为已终止 | +| PMS | 不调用,不回滚已完成卡 | +| 异常标记 | 关联订单标记为异常订单 | +| 后续新任务 | 仍可查看内容,但状态为异常阻断;无确认入口,不允许调用 PMS | +| 终止记录 | 固定原因“人工终止” + 终止时间 | +| 终止人 | 当前无账户架构,不记录、不显示、不造占位值 | +| 纯通知 | 不提供终止入口 | + +## 十二、状态汇总 + +### 1. 单卡状态 + +| 状态 | 含义 | +|---|---| +| 待确认 | 卡片已生成,等待用户检查和确认 | +| 待确认 + 需人工复核 | Agent 明确要求用户核对或补正该卡;仍是待确认卡 | +| 已完成 | 卡片已完成信息系统当前阶段要求的处理并永久锁定 | +| 已终止 | 订单任务被人工终止时,该卡尚未完成 | + +### 2. 订单任务汇总状态 + +| 条件 | 汇总状态 | +|---|---| +| 所有可执行卡均未完成 | 待处理 | +| 至少一张完成且仍有卡未完成 | 处理中 | +| 所有可执行卡均完成 | 已完成 | +| 任一卡触发订单级终止 | 已终止 | +| 异常订单出现后续新任务 | 异常阻断 | + +订单任务不显示“已完成 2/5”等计数;单卡的“需人工复核”不形成订单级额外提示;普通预订中的邮件证据卡不参与进度汇总。 + +## 十三、当前无 PMS API 的统一结果口径 + +| 业务卡 | 信息系统当前可以做什么 | 当前不能宣称什么 | +|---|---|---| +| Basic Information | 保存最终 Account、Market、Source | PMS 已接收 | +| New Booking | 保存完整目标订单参数 | 创建成功、已有 Block ID / Confirmation Number | +| Update Booking | 保存最终 After,并更新本地订单最新数据 | PMS 已更新 | +| Cancel Booking | 保留历史并把本地订单标记为已取消 | PMS 已取消 | +| Trace | 保存最终事项 | 部门已同步、PMS 已写入、加床已生效 | +| Rooming List | 完成标准表格展示确认;Group 本地状态处理为 DEF | 名单已准确转换或已导入 PMS | +| Payment | 展示凭证并完成当前本地处理;Group 本地状态处理为 DEF | 已到账、Payment Confirmed、PMS 成功 | +| 纯通知 | 记录用户已确认查看和处理 | 任何订单或 PMS 动作已执行 | + +统一原则:页面中的“已完成”只表示该任务卡已经完成信息系统当前阶段要求的确认和本地处理,不等于 PMS 成功。 + +## 十四、明确排除与后置事项 + +- 根级 Invoice 不在本轮 Agent 预订任务范围; +- 根级 `/rooming-list` 文件工作区不在本轮 Agent 预订任务范围; +- 不存在独立 Voucher、S99、Fallback、Need Manual Review 或统一“条件业务卡片”; +- 当前不设计数据库、接口、技术模块、代码结构或 PMS 重试机制; +- 真实 PMS API 参数、执行回执和失败处理拿到 API 后再确认; +- Rooming List 的真实转换和住客数据结构拿到 API 后再确认; +- Agent 最终 JSON Schema、训练案例和 Skill 调试不属于本次开发业务交接包。 diff --git a/docs/import/20260718/0718给黄哥/03-业务验收场景清单.md b/docs/import/20260718/0718给黄哥/03-业务验收场景清单.md new file mode 100644 index 0000000..9b6ea94 --- /dev/null +++ b/docs/import/20260718/0718给黄哥/03-业务验收场景清单.md @@ -0,0 +1,273 @@ +# 预订任务信息系统业务验收场景清单 + +版本:V1.0 +日期:2026-07-18 +适用阶段:无 PMS API + +本清单用于需求评审、开发自测和业务验收。页面视觉可参考 `04-前端视觉参考/`,业务判断以主说明书为准。 + +## 一、任务拆分与卡片组合 + +### AC-001 只有 New Booking + +- 前提:Agent 只识别出一笔 New Booking,没有 Trace、Rooming List 或 Payment。 +- 期望:页面依次显示 Basic Information、房间信息、邮件展示;不显示任何空可选卡。 + +### AC-002 同订单多业务事件 + +- 前提:同一来源邮件、同一目标订单同时包含 Update、Trace 和 Payment。 +- 期望:聚合为一个订单任务;显示 Basic Information、房间信息、Trace、Payment、邮件展示;各业务卡独立确认。 + +### AC-003 同邮件多个订单 + +- 前提:一封邮件分别涉及两个目标订单。 +- 期望:拆成两笔订单任务;每笔只显示自己的业务事件与附件关联,不根据事件顺序猜测归属。 + +### AC-004 纯通知 + +- 前提:没有形成具体预订业务任务。 +- 期望:只显示邮件展示卡;不显示 Basic Information、房间信息、人工复核卡或终止入口。 + +## 二、Basic Information + +### AC-005 Account 正常匹配 + +- 前提:Account 可匹配信息系统目录。 +- 期望:Market、Source 自动带出;三项均为受控选择,用户可改选已有值;没有 Manual 和自由输入。 + +### AC-006 Account 无法确定 + +- 前提:Agent 已识别具体预订业务,但无法可靠确定 Account。 +- 期望:Basic Information 显示“待确认 + 需人工复核”;用户只能从已有目录补选,不能输入新目录值。 + +## 三、New Booking + +### AC-007 Group 普通新建 + +- 前提:Group New Booking,普通预订。 +- 期望:Block Name 显示 Group Code;Group Booking Status 初始为 TEN;房型行按 RoomType、房量、Adult(每间)显示。 + +### AC-008 Group Proposal + +- 前提:Group New Booking,邮件为 Proposal。 +- 期望:Group Booking Status 初始为 INQ。 + +### AC-009 Fit 已有真实姓名 + +- 前提:Fit 首封邮件同时提供 Booking Code 和真实住客姓名。 +- 期望:Name 直接显示真实姓名;已有订单后续操作不使用 Name 定位。 + +### AC-010 Fit 暂无真实姓名 + +- 前提:Fit 首封邮件只有 Booking Code,没有真实住客姓名。 +- 期望:Name 临时使用 Booking Code;该情况信息完整,不触发人工复核。 + +### AC-011 New 无 PMS API确认 + +- 操作:用户确认房间信息卡。 +- 期望:保存完整目标参数,卡片已完成并锁定;显示“信息系统已确认,未调用 PMS”;Block ID / Confirmation Number 显示“未接入 PMS API”,不得伪造成功。 + +## 四、Update Booking + +### AC-012 修改日期 + +- 前提:邮件要求修改入住和/或离店日期。 +- 期望:页面突出原日期到修改后日期;Nights 按新日期自动计算;完整最新订单仍可查看。 + +### AC-013 修改房型或房量 + +- 前提:原订单为 `TWN × 5 + DBL × 5`,目标改为 `TWN × 3 + DBL × 5`。 +- 期望:页面显示修改前和修改后的完整房型清单;不得把仅 `TWN × 3` 当成完整目标而丢失 DBL。 + +### AC-014 Rate Code 不可在 Update 修改 + +- 前提:订单已经建立。 +- 期望:Rate Code 可作为当前订单信息显示,但不提供原值到新值的修改入口。 + +### AC-015 Fit Name 更新 + +- 前提:Fit 原 Name 为 Booking Code,后续邮件提供真实姓名和 Confirmation Number。 +- 期望:通过 Confirmation Number 定位原订单,并以 Update 更新 Name;不创建第二笔订单。 + +### AC-016 Update 无 PMS API确认 + +- 操作:用户确认 Update 房间信息卡。 +- 期望:卡片完成并锁定;最终变化写入信息系统本地订单最新数据;后续本地查单读取新数据;页面明确未调用 PMS。 + +## 五、Cancel Booking + +### AC-017 整单取消 + +- 前提:Agent 识别为 Cancel Booking并唯一找到订单。 +- 期望:完整订单只读展示,只出现“将取消整笔订单”,没有修改后参数。 + +### AC-018 部分调整不是取消 + +- 前提:邮件只减少房量、删除房型或修改日期。 +- 期望:业务类型为 Update Booking,不是 Cancel Booking。 + +### AC-019 Cancel 无 PMS API确认 + +- 操作:用户确认 Cancel 房间信息卡。 +- 期望:保留订单和历史,本地标记已取消,页面明确未调用 PMS;后续 Update、Trace、Rooming List、Payment 仍可查看结构但全部禁止确认。 + +## 六、Trace + +### AC-020 普通 Trace + +- 前提:邮件要求蜜月布置并指定相关部门。 +- 期望:一条普通事项显示事项内容和受控 Department;不显示 RoomType、加床数量或 Adult;内容和 Department 可在确认前修改。 + +### AC-021 单数加床 + +- 前提:邮件只写“请给 TWN 加床”,没有其他数量。 +- 期望:加床数量默认为 1;显示 `TWN × 1|Adult 3`(以基础 Adult 2 为例)及 Department;事项默认文本为 `SET EXTRA BED`。 + +### AC-022 加床与普通事项并存 + +- 前提:同一订单同时要求加床和蜜月布置。 +- 期望:只显示一张 Trace 卡,卡内分别显示一条加床事项和一条普通事项,不丢失任一事项。 + +### AC-023 Trace 目标房型错误 + +- 前提:加床目标 RoomType 不存在于当前目标预订或已调出的订单。 +- 期望:整笔订单任务进入人工核对;系统不能只改成一条普通文本备注继续确认。 + +### AC-024 Trace 无 PMS API确认 + +- 操作:用户确认 Trace 卡。 +- 期望:保存全部最终事项,卡片完成并锁定;不改写房间汇总,不宣称部门已同步或加床已生效。 + +## 七、Rooming List + +### AC-025 Group Rooming List + +- 前提:Agent 识别出 Group Rooming List 并唯一找到 Group Code。 +- 期望:显示 Rooming List 卡和 12 个规定表头;原附件只在邮件展示卡查看;Fit 不生成该卡。 + +### AC-026 新版 Rooming List + +- 前提:同一 Group 后续收到新版 Rooming List 邮件。 +- 期望:创建新任务并关联原订单;旧任务、旧邮件和旧确认结果保留。 + +### AC-027 当前简化范围 + +- 期望:系统无需证明已经把附件真实转换为 PMS 数据,不要求 Excel 下载、复杂住客编辑或手工 PMS 导入;确认后卡片锁定并把本地 Group Booking Status 处理为 DEF。 + +## 八、Payment + +### AC-028 一笔订单多份凭证 + +- 前提:同一 Payment 关联多份来源附件。 +- 期望:在一张 Payment 卡中展示全部凭证,不按图片拆卡;附件顺序不改变业务含义。 + +### AC-029 Group Payment + +- 操作:用户查看凭证后点击“确认”。 +- 期望:Payment 卡完成并锁定,本地 Group Booking Status 更新为 DEF;不显示 Payment Confirmed,不代表 PMS 或到账确认。 + +### AC-030 Fit Payment + +- 操作:用户查看凭证后点击“确认”。 +- 期望:Payment 卡完成并锁定,不增加 Group Booking Status 或其他付款字段。 + +## 九、邮件展示 + +### AC-031 普通预订邮件证据卡 + +- 期望:显示当前邮件的主题、发件人、时间、原文和附件;全部只读,附件只可预览或下载;不显示“确认”。 + +### AC-032 完整邮件线程 + +- 操作:用户点击“查看完整邮件”。 +- 期望:进入该邮件会话;任务邮件卡本身仍只展开当前来源邮件。 + +### AC-033 纯通知确认 + +- 操作:用户仅打开页面。 +- 期望:任务不自动完成。用户点击“确认”后才完成并锁定,不调用 PMS。 + +## 十、人工复核与查单校验 + +### AC-034 单卡人工复核 + +- 前提:Trace 需要人工复核,Basic Information 与房间信息合法。 +- 期望:只有 Trace 显示“待确认 + 需人工复核”并被阻止确认;其他卡可独立确认;订单顶部不增加人工复核状态。 + +### AC-035 人工复核一步确认 + +- 前提:用户已按既定权限补正卡片数据。 +- 期望:直接点击同一个“确认”完成;不存在“解除复核”或二次确认。 + +### AC-036 查到 0 笔或多笔订单 + +- 前提:Agent 没有要求人工复核,但系统未唯一找到已有订单。 +- 期望:卡片保持“待确认”;仅定位字段可编辑,其余可见但禁用;显示统一查单提示,不追加“需人工复核”。 + +## 十一、确认、锁定与终止 + +### AC-037 多卡独立确认 + +- 前提:同一订单任务有 Basic、房间信息和 Trace。 +- 操作:先确认 Basic。 +- 期望:只有 Basic 完成并锁定;房间信息和 Trace 仍保持原状态;订单任务显示“处理中”。 + +### AC-038 无草稿 + +- 操作:用户修改未确认字段后离开页面再返回。 +- 期望:未确认修改不保留,仍从 Agent 原始值开始;页面没有“保存草稿”。 + +### AC-039 确认后不可修改 + +- 前提:某卡已完成。 +- 期望:不能再次编辑、确认或重新执行,也不能从该卡创建新任务。 + +### AC-040 部分卡完成后终止 + +- 前提:Basic 已完成,房间信息与 Trace 未完成。 +- 操作:用户从 Trace 点击“终止任务”。 +- 期望:Basic 保持已完成;房间信息和 Trace 变为已终止;订单任务已终止并标记异常;终止不回滚已完成卡。 + +### AC-041 纯通知没有终止 + +- 前提:任务只显示邮件展示卡。 +- 期望:只有“确认”,没有“终止任务”。 + +## 十二、异常阻断与技术异常 + +### AC-042 异常订单后续 Update + +- 前提:订单曾因人工终止被标记异常,后续 Agent 生成新的 Update。 +- 期望:页面可查看邮件和卡片,但任务状态为“异常阻断”;没有可用确认或终止入口,不调用 PMS。 + +### AC-043 AMEND GROUP CODE + +- 前提:邮件要求修改已创建 Group 的 Group Code。 +- 期望:作为纯通知,只显示邮件卡;用户在 PMS 外部处理后回系统点击“确认”;不创建旧/新 Group Code 业务卡。 + +### AC-044 Agent 技术契约错误 + +- 前提:信息系统收到无法安全解析的 Agent 技术输出。 +- 期望:不创建酒店用户可见任务、列表行或卡片;交由开发处理,不转换成纯通知或人工复核。 + +## 十三、订单任务汇总状态 + +### AC-045 全部未完成 + +- 期望:订单任务显示“待处理”。 + +### AC-046 部分完成 + +- 期望:订单任务显示“处理中”,不显示完成数量。 + +### AC-047 全部完成 + +- 期望:订单任务显示“已完成”。 + +### AC-048 任一卡终止 + +- 期望:订单任务显示“已终止”,未完成卡已终止,已完成卡保持已完成。 + +### AC-049 异常订单的新任务 + +- 期望:订单任务显示“异常阻断”,不是待处理、人工复核或已终止。 diff --git a/docs/import/20260718/0718给黄哥/04-前端视觉参考/00-视觉参考说明.md b/docs/import/20260718/0718给黄哥/04-前端视觉参考/00-视觉参考说明.md new file mode 100644 index 0000000..9e180cf --- /dev/null +++ b/docs/import/20260718/0718给黄哥/04-前端视觉参考/00-视觉参考说明.md @@ -0,0 +1,46 @@ +# 前端视觉参考说明 + +本文件夹保存 2026-07-18 当前前端页面截图,用于帮助开发理解页面布局、卡片层级、字段组织和视觉样式。 + +## 使用原则 + +- 截图只作为视觉参考,不是业务规则权威来源; +- 若截图中的字段、状态、按钮或旧文案与 `01-预订任务信息系统业务需求说明书` 冲突,以主说明书为准; +- 截图可能包含演示 fixture,不代表真实 Agent 输出、真实订单或 PMS 执行结果; +- 根级 Invoice 和根级 Rooming List 不属于本次预订任务业务说明范围,不纳入本文件夹。 + +## 截图与用途 + +| 文件 | 主要参考内容 | +|---|---| +| `01-任务列表-当前视觉.png` | 整体导航、任务概览、列表层级、状态标签与行操作 | +| `02-NewBooking-任务详情-当前视觉.png` | Group New Booking 的详情页结构、Basic Information、房间信息和邮件展示 | +| `03-Update与Trace-任务详情-当前视觉.png` | Update 的 Before → After 强调方式、普通 Trace 的卡片层级 | +| `04-Payment-任务详情-当前视觉.png` | Payment 凭证图片预览、人工复核提示与邮件证据布局 | +| `05-纯通知-任务详情-当前视觉.png` | 只显示邮件展示卡的纯通知页面 | +| `06-CancelBooking-任务详情-当前视觉.png` | 整单取消提示及当前订单只读展示方式 | +| `07-加床Trace-任务详情-当前视觉.png` | 加床 Trace 与普通 Trace 同卡共存,以及 RoomType、数量、Adult、Department 的组合展示 | + +## 已知差异,开发必须按主文档修正 + +当前前端是在业务规则最终收口前制作的原型,因此截图中仍有历史内容: + +- 所有可执行任务卡的最终按钮文案统一为“确认”,不使用“执行任务”“已查看”或“确认修改”; +- 不提供“保存草稿”;离开页面后未确认修改无需保留; +- 人工复核补正后直接点击同一个“确认”,不存在先确认修改、再执行的两步流程; +- 每张任务卡独立确认和锁定,不能用一个页面底部动作代替所有卡片的独立操作; +- 确认后永久锁定,不支持旧卡再次编辑、再次执行或重试; +- Voucher 已并入 Payment,不存在独立 Voucher 卡; +- Payment 当前只需展示对应来源凭证,不需要截图原型中遗留的其他付款专属字段; +- 纯通知卡的按钮也统一为“确认”,并且没有人工终止; +- 根级 Rooming List 页面与本轮任务详情内嵌 Rooming List 无关,不能作为后者的实现依据。 + +## Rooming List 视觉缺口 + +当前任务详情原型尚未完整实现最新的内嵌 Rooming List 标准表格,因此本文件夹不放一张容易误导开发的旧版 Rooming List 截图,也不使用根级 `/rooming-list` 页面替代。 + +当前版本应直接按主说明书和字段矩阵实现:Rooming List 卡展示 12 个必填业务字段的标准表格示意;原始附件只在邮件展示卡查看;当前不要求真实转换、复杂编辑或 Excel 流程。 + +## 状态页面说明 + +本次没有为“已完成、已终止、异常阻断”逐一制造截图。它们的业务规则与操作权限应直接按主说明书和验收清单实现,不能从现有 fixture 的历史状态倒推。 diff --git a/docs/import/20260718/0718给黄哥/04-前端视觉参考/01-任务列表-当前视觉.png b/docs/import/20260718/0718给黄哥/04-前端视觉参考/01-任务列表-当前视觉.png new file mode 100644 index 0000000..019c2a6 Binary files /dev/null and b/docs/import/20260718/0718给黄哥/04-前端视觉参考/01-任务列表-当前视觉.png differ diff --git a/docs/import/20260718/0718给黄哥/04-前端视觉参考/02-NewBooking-任务详情-当前视觉.png b/docs/import/20260718/0718给黄哥/04-前端视觉参考/02-NewBooking-任务详情-当前视觉.png new file mode 100644 index 0000000..7a1b66a Binary files /dev/null and b/docs/import/20260718/0718给黄哥/04-前端视觉参考/02-NewBooking-任务详情-当前视觉.png differ diff --git a/docs/import/20260718/0718给黄哥/04-前端视觉参考/03-Update与Trace-任务详情-当前视觉.png b/docs/import/20260718/0718给黄哥/04-前端视觉参考/03-Update与Trace-任务详情-当前视觉.png new file mode 100644 index 0000000..b0390bb Binary files /dev/null and b/docs/import/20260718/0718给黄哥/04-前端视觉参考/03-Update与Trace-任务详情-当前视觉.png differ diff --git a/docs/import/20260718/0718给黄哥/04-前端视觉参考/04-Payment-任务详情-当前视觉.png b/docs/import/20260718/0718给黄哥/04-前端视觉参考/04-Payment-任务详情-当前视觉.png new file mode 100644 index 0000000..2c27c2d Binary files /dev/null and b/docs/import/20260718/0718给黄哥/04-前端视觉参考/04-Payment-任务详情-当前视觉.png differ diff --git a/docs/import/20260718/0718给黄哥/04-前端视觉参考/05-纯通知-任务详情-当前视觉.png b/docs/import/20260718/0718给黄哥/04-前端视觉参考/05-纯通知-任务详情-当前视觉.png new file mode 100644 index 0000000..6b7bab5 Binary files /dev/null and b/docs/import/20260718/0718给黄哥/04-前端视觉参考/05-纯通知-任务详情-当前视觉.png differ diff --git a/docs/import/20260718/0718给黄哥/04-前端视觉参考/06-CancelBooking-任务详情-当前视觉.png b/docs/import/20260718/0718给黄哥/04-前端视觉参考/06-CancelBooking-任务详情-当前视觉.png new file mode 100644 index 0000000..3832246 Binary files /dev/null and b/docs/import/20260718/0718给黄哥/04-前端视觉参考/06-CancelBooking-任务详情-当前视觉.png differ diff --git a/docs/import/20260718/0718给黄哥/04-前端视觉参考/07-加床Trace-任务详情-当前视觉.png b/docs/import/20260718/0718给黄哥/04-前端视觉参考/07-加床Trace-任务详情-当前视觉.png new file mode 100644 index 0000000..9266174 Binary files /dev/null and b/docs/import/20260718/0718给黄哥/04-前端视觉参考/07-加床Trace-任务详情-当前视觉.png differ diff --git a/docs/project/frontend-backend/backend-to-frontend-notes.md b/docs/project/frontend-backend/backend-to-frontend-notes.md index 6a70879..1b16932 100644 --- a/docs/project/frontend-backend/backend-to-frontend-notes.md +++ b/docs/project/frontend-backend/backend-to-frontend-notes.md @@ -432,9 +432,10 @@ RESERVATION_ROOMING_LIST_GENERATE - 姓名支持 `LI/CHUNHONG` 和 `LI CHUNHONG` 两类格式;后端会把第一段写入目标 `Name`,剩余部分写入目标 `First Name`。 - 前端必须让用户输入 `people_per_room`,后端按名单顺序分组,并用 `ceil(total_people / people_per_room)` 生成房间行。 - 每组第一位旅客写入 `Name` / `First Name`,同组剩余旅客写入 `Accompanying Guests`,多人用英文逗号分隔。 -- 除 `Line`、`Name`、`First Name`、`Number of Rooms`、`Accompanying Guests` 外,目标 Excel 其他字段第一版都由前端输入或选择,例如 `Arrival`、`Departure`、`Room Type`、`Rate Code`、`Payment Type`、`Nationality`。 +- 除 `Line`、`Name`、`First Name`、`Number of Rooms`、`Accompanying Guests` 外,目标 Excel 其他字段第一版都由前端输入或选择,例如 `Title`、`Arrival`、`Departure`、`Room Type`、`Rate Code`、`Payment Type`、`Nationality`。 - 第一版接口成功后直接返回 Excel 文件流,前端应按 Blob 下载处理,不要期待 JSON 里的 URL。 - 失败时返回统一 JSON 错误,例如 `ROOMING_LIST_VALIDATION_FAILED`、`ROOMING_LIST_SOURCE_FILE_INVALID`、`HOTEL_ACCESS_DENIED` 或 `FRONTEND_PERMISSION_DENIED`。 +- 该上传接口会在 multipart 参数绑定前先校验 Bearer token 和 `RESERVATION_ROOMING_LIST_GENERATE` 权限;未登录时优先返回 401,不会因为缺少业务字段先返回 400。 - `ROOMING_LIST_VALIDATION_FAILED` 已覆盖 multipart 必填字段缺失、日期格式错误和数字格式错误,`details[]` 会返回字段级提示。 - 页面不要把上传文件内容、客人名单、生成文件内容写入浏览器日志、埋点、错误上报、URL 或 localStorage。 - 第一版没有 preview 接口、生成记录接口、OSS URL、历史下载和订单 / 任务预填;前端不要在页面上承诺这些能力。 diff --git a/docs/project/requirements/M010-rooming-list-excel-generation-v1.md b/docs/project/requirements/M010-rooming-list-excel-generation-v1.md index 664e570..f0ccfd7 100644 --- a/docs/project/requirements/M010-rooming-list-excel-generation-v1.md +++ b/docs/project/requirements/M010-rooming-list-excel-generation-v1.md @@ -160,6 +160,7 @@ Content-Type: multipart/form-data | `arrival` | string | 是 | 入住日期,酒店本地业务日期,格式 `yyyy-MM-dd` | | `departure` | string | 是 | 离店日期,酒店本地业务日期,格式 `yyyy-MM-dd` | | `room_type` | string | 是 | 目标 Excel 的 `Room Type` | +| `title` | string | 否 | 目标 Excel 的 `Title` | | `rate_code` | string | 否 | 目标 Excel 的 `Rate Code` | | `adults` | integer | 否 | 目标 Excel 的 `Adults`;为空时可由后端按分组人数派生 | | `children` | integer | 否 | 目标 Excel 的 `Children`;为空时默认 `0` | @@ -212,6 +213,7 @@ RESERVATION_ROOMING_LIST_GENERATE - 必须登录。 - 必须拥有 `RESERVATION_ROOMING_LIST_GENERATE` 权限。 +- 上传接口在 multipart 参数绑定前执行登录和权限前置校验,未登录或无权限请求不会进入来源文件解析。 - 必须校验用户对 `hotel_id` 的访问权。 - 第一版直接下载、不落库,因此不写生成记录表。 - 当前 CP1 不新增持久化审计表;后续如增加生成记录或历史下载,再补业务审计落库。 diff --git a/docs/project/security-access-control-boundary.md b/docs/project/security-access-control-boundary.md index 72b99e8..36abd82 100644 --- a/docs/project/security-access-control-boundary.md +++ b/docs/project/security-access-control-boundary.md @@ -55,7 +55,7 @@ | `POST /api/reservation/tasks/{taskId}/opera-operations/{operationId}/retry` | `FRONTEND_USER` | 当前为 OPERA 模拟 | 登录 + `RESERVATION_OPERA_SIM_EXECUTE` + 酒店访问权 | 必须写业务审计和 attempt | | `GET /api/reservation/tasks/{taskId}/audits` | `FRONTEND_USER` | 已强制 Bearer 登录 + `RESERVATION_AUDIT_READ`;按任务实际所属酒店校验访问权 | 保持登录 + `RESERVATION_AUDIT_READ` + 酒店访问权 | 查询审计不再写审计 | | `POST /api/reservation/invoices/manual-generations` | `FRONTEND_USER` | 已实现 M009 CP2;强制 Bearer 登录 + `RESERVATION_INVOICE_GENERATE` + 酒店访问权;`task_id` / `order_id` 可为空,传入时反查对象所属酒店 | 保持登录 + `RESERVATION_INVOICE_GENERATE` + 酒店访问权;后续如增加历史列表或预填接口需单独登记权限 | 写业务审计,记录来源类型、模板版本、生成结果摘要;生成失败写入生成记录安全错误摘要 | -| `POST /api/reservation/rooming-lists/generations` | `FRONTEND_USER` | 已实现 M010 CP1;multipart 上传来源名单并直接下载 `.xlsx` | 登录 + `RESERVATION_ROOMING_LIST_GENERATE` + 酒店访问权;第一版不落库、不上传 OSS | CP1 不落生成记录表;错误响应不得记录完整名单和证件信息,后续若增加历史记录再补业务审计 | +| `POST /api/reservation/rooming-lists/generations` | `FRONTEND_USER` | 已实现 M010 CP1;multipart 上传来源名单并直接下载 `.xlsx`;已在 multipart 参数绑定前前置校验登录和生成权限 | 登录 + `RESERVATION_ROOMING_LIST_GENERATE` + 酒店访问权;第一版不落库、不上传 OSS | CP1 不落生成记录表;错误响应不得记录完整名单和证件信息,后续若增加历史记录再补业务审计 | ### 3.3 来源邮件接口 diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/request/ReservationRoomingListGenerationRequest.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/request/ReservationRoomingListGenerationRequest.java index 3642263..cb58ca2 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/request/ReservationRoomingListGenerationRequest.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/request/ReservationRoomingListGenerationRequest.java @@ -10,6 +10,7 @@ import java.time.LocalDate; * @param arrival 入住日期,酒店本地业务日期 * @param departure 离店日期,酒店本地业务日期 * @param roomType 目标 Excel 的 Room Type + * @param title 目标 Excel 的 Title * @param rateCode 目标 Excel 的 Rate Code * @param adults 目标 Excel 的 Adults;为空时按当前分房人数派生 * @param children 目标 Excel 的 Children;为空时默认 0 @@ -26,6 +27,7 @@ public record ReservationRoomingListGenerationRequest( LocalDate arrival, LocalDate departure, String roomType, + String title, String rateCode, Integer adults, Integer children, diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationRoomingListGenerationAuthorizationFilter.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationRoomingListGenerationAuthorizationFilter.java new file mode 100644 index 0000000..84896d2 --- /dev/null +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationRoomingListGenerationAuthorizationFilter.java @@ -0,0 +1,124 @@ +package cn.nianxx.thhotel.workflows.reservation.control; + +import cn.nianxx.thhotel.platform.access.common.enums.PlatformPermissionCode; +import cn.nianxx.thhotel.platform.identity.service.AuthService; +import cn.nianxx.thhotel.platform.security.common.dto.AuthenticatedUserContext; +import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationWorkflowErrorResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Optional; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Rooming List 上传接口前置鉴权过滤器。先校验登录和权限,再允许 multipart 参数绑定。 + */ +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class ReservationRoomingListGenerationAuthorizationFilter extends OncePerRequestFilter { + + private static final String ENDPOINT = "/api/reservation/rooming-lists/generations"; + + private final AuthService authService; + private final ObjectMapper objectMapper; + + /** + * 注入认证服务和 JSON 序列化器。 + */ + public ReservationRoomingListGenerationAuthorizationFilter( + AuthService authService, + ObjectMapper objectMapper) { + this.authService = authService; + this.objectMapper = objectMapper; + } + + /** + * 对 Rooming List 上传接口提前执行登录和权限校验,避免未登录请求进入 multipart 绑定。 + */ + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + if (!matchesRoomingListGeneration(request)) { + filterChain.doFilter(request, response); + return; + } + + String authorizationHeader = request.getHeader("Authorization"); + Optional context = authService.resolveOptionalContext(authorizationHeader); + if (context.isEmpty()) { + writeError( + response, + HttpStatus.UNAUTHORIZED, + hasBearerToken(authorizationHeader) ? "AUTH_SESSION_INVALID" : "AUTH_TOKEN_REQUIRED", + hasBearerToken(authorizationHeader) ? "登录已失效,请重新登录。" : "请先登录后再访问该业务能力。"); + return; + } + if (!context.get().permissionCodes() + .contains(PlatformPermissionCode.RESERVATION_ROOMING_LIST_GENERATE.name())) { + writeError( + response, + HttpStatus.FORBIDDEN, + "FRONTEND_PERMISSION_DENIED", + "当前用户没有访问该业务能力的权限。"); + return; + } + + filterChain.doFilter(request, response); + } + + /** + * 判断当前请求是否为 Rooming List 生成接口。 + */ + private boolean matchesRoomingListGeneration(HttpServletRequest request) { + if (!"POST".equalsIgnoreCase(request.getMethod())) { + return false; + } + String contextPath = request.getContextPath(); + String requestUri = request.getRequestURI(); + String path = requestUri; + if (contextPath != null && !contextPath.isBlank() && requestUri.startsWith(contextPath)) { + path = requestUri.substring(contextPath.length()); + } + return ENDPOINT.equals(path); + } + + /** + * 写入前端业务接口统一错误响应。 + */ + private void writeError( + HttpServletResponse response, + HttpStatus status, + String errorCode, + String message) throws IOException { + response.setStatus(status.value()); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + objectMapper.writeValue( + response.getWriter(), + new ReservationWorkflowErrorResponse(errorCode, message, List.of())); + } + + /** + * 判断请求头中是否携带非空 Bearer token。 + */ + private boolean hasBearerToken(String authorizationHeader) { + if (authorizationHeader == null || authorizationHeader.isBlank()) { + return false; + } + String prefix = "Bearer "; + return authorizationHeader.regionMatches(true, 0, prefix, 0, prefix.length()) + && !authorizationHeader.substring(prefix.length()).trim().isBlank(); + } +} diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationRoomingListGenerationController.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationRoomingListGenerationController.java index 7d16791..ffe6772 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationRoomingListGenerationController.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationRoomingListGenerationController.java @@ -49,6 +49,7 @@ public class ReservationRoomingListGenerationController { @RequestParam("arrival") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate arrival, @RequestParam("departure") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate departure, @RequestParam("room_type") String roomType, + @RequestParam(value = "title", required = false) String title, @RequestParam(value = "rate_code", required = false) String rateCode, @RequestParam(value = "adults", required = false) Integer adults, @RequestParam(value = "children", required = false) Integer children, @@ -68,6 +69,7 @@ public class ReservationRoomingListGenerationController { arrival, departure, roomType, + title, rateCode, adults, children, diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationRoomingListGenerationServiceImpl.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationRoomingListGenerationServiceImpl.java index f328607..c9cbea7 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationRoomingListGenerationServiceImpl.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationRoomingListGenerationServiceImpl.java @@ -113,6 +113,7 @@ public class ReservationRoomingListGenerationServiceImpl implements ReservationR request.arrival(), request.departure(), trimToEmpty(request.roomType()), + trimToEmpty(request.title()), trimToEmpty(request.rateCode()), request.adults(), request.children(), diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/RoomingListExcelRenderer.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/RoomingListExcelRenderer.java index b8cdcf9..c69c4a3 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/RoomingListExcelRenderer.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/RoomingListExcelRenderer.java @@ -90,7 +90,7 @@ public class RoomingListExcelRenderer { row.createCell(0).setCellValue(room.lineNumber()); row.createCell(1).setCellValue(room.primaryGuest().lastName()); row.createCell(2).setCellValue(room.primaryGuest().firstName()); - row.createCell(3).setCellValue(""); + row.createCell(3).setCellValue(defaultString(request.title())); row.createCell(4).setCellValue(request.arrival().format(DateTimeFormatter.ISO_LOCAL_DATE)); row.createCell(5).setCellValue(request.departure().format(DateTimeFormatter.ISO_LOCAL_DATE)); row.createCell(6).setCellValue(defaultString(request.roomType())); diff --git a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationRoomingListGenerationControllerTest.java b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationRoomingListGenerationControllerTest.java index 350a5d1..73f5eba 100644 --- a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationRoomingListGenerationControllerTest.java +++ b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationRoomingListGenerationControllerTest.java @@ -91,6 +91,7 @@ class ReservationRoomingListGenerationControllerTest { .param("arrival", "2026-07-26") .param("departure", "2026-07-29") .param("room_type", "UG1") + .param("title", "MR") .param("rate_code", "RACK") .param("payment_type", "CA") .param("nationality", "CN")) @@ -101,6 +102,8 @@ class ReservationRoomingListGenerationControllerTest { .andExpect(header().string("Content-Disposition", containsString("rooming-list-HOTEL-TEST-"))) .andExpect(result -> assertThat(result.getResponse().getContentAsByteArray()) .startsWith(new byte[]{0x50, 0x4B})) + .andExpect(result -> assertThat(titleCell(result.getResponse().getContentAsByteArray())) + .isEqualTo("MR")) .andExpect(content().string(not(containsString("P123456")))); } @@ -117,6 +120,18 @@ class ReservationRoomingListGenerationControllerTest { .andExpect(jsonPath("$.error_code").value("AUTH_TOKEN_REQUIRED")); } + @Test + void shouldRejectRoomingListGenerationWhenTokenMissingBeforeBindingErrors() throws Exception { + mockMvc.perform(multipart(ENDPOINT) + .file(sourceFile()) + .param("hotel_id", "HOTEL-TEST") + .param("people_per_room", "2") + .param("arrival", "2026-07-26") + .param("departure", "2026-07-29")) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.error_code").value("AUTH_TOKEN_REQUIRED")); + } + @Test void shouldRejectRoomingListGenerationWhenPermissionMissing() throws Exception { String token = loginToken(mockMvc, "rooming-no-permission", "NoPerm@123456"); @@ -199,4 +214,10 @@ class ReservationRoomingListGenerationControllerTest { outputStream.toByteArray()); } } + + private String titleCell(byte[] workbookBytes) throws Exception { + try (XSSFWorkbook workbook = new XSSFWorkbook(new java.io.ByteArrayInputStream(workbookBytes))) { + return workbook.getSheetAt(0).getRow(1).getCell(3).getStringCellValue(); + } + } } diff --git a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationRoomingListGenerationServiceImplTest.java b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationRoomingListGenerationServiceImplTest.java index ea95267..c4ce625 100644 --- a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationRoomingListGenerationServiceImplTest.java +++ b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationRoomingListGenerationServiceImplTest.java @@ -60,6 +60,7 @@ class ReservationRoomingListGenerationServiceImplTest { assertThat(cellText(firstRoom.getCell(0))).isEqualTo("1"); assertThat(cellText(firstRoom.getCell(1))).isEqualTo("LI"); assertThat(cellText(firstRoom.getCell(2))).isEqualTo("CHUNHONG"); + assertThat(cellText(firstRoom.getCell(3))).isEqualTo("MR"); assertThat(cellText(firstRoom.getCell(5))).isEqualTo("2026-07-29"); assertThat(cellText(firstRoom.getCell(6))).isEqualTo("UG1"); assertThat(cellText(firstRoom.getCell(8))).isEqualTo("1"); @@ -121,6 +122,7 @@ class ReservationRoomingListGenerationServiceImplTest { LocalDate.of(2026, 7, 26), LocalDate.of(2026, 7, 29), "UG1", + "MR", "RACK", null, 0,