diff --git a/.gitignore b/.gitignore index 7fa1595..4d58e28 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ server/target/ var/ *.log +.superpowers/ diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index e8d28d3..02cbbda 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -2,10 +2,10 @@ | 项 | 内容 | | --- | --- | -| 最近更新 | 2026-07-21 | +| 最近更新 | 2026-07-22 | | 当前分支 | `feature/huangting` | | 当前阶段 | M002 V4 入站、多卡模型、持久化基线、入站写入、查询接口、卡片确认、复核解阻、目录校验、订单详情 V4 总览、DB 目录、Lookup API、前端 lookup 接入、目录管理后台 CP1 前后端、订单列表 V4 继续处理入口 / open count 收口、V4 业务审计查询、停止旧任务双写、Debug EML V4 profile 对齐、Room Information 后端展示模型与前端业务化展示、V4 任务详情 smoke 修复、Rooming List 确认自动 DEF 后端联动、Rooming List 前端轻量事项卡、Room Information 复核 pointer 与任务详情安全边界修复、Room Information 复核 pointer 运行时规则收口、复核 pointer 部署证明与运行时 trace、OWNER RATE Room Type / Rate Code 目录口径、Payment 附件预览后端安全摘要与前端预览接入、Trace 卡后端字段契约收口、Trace 确认态字段刷新、Rooming List 事项确认卡文档口径、V4 复核态卡片交互和字段白名单文档口径,以及 V4 工作台 / 订单详情 / 任务详情普通酒店员工用户化展示与 polish 收口 | -| 当前重点 | M002 V4 已停止普通业务入站双写旧 `workflow_reservation_task`,V4 后新业务主线只写 V4 order task / cards / source notification;Debug EML V4 smoke 默认复用实时 AgentBus V4 Open API subject,避免误走历史 Debug V2/V3 profile。开发阶段不维护 V2/V3 旧任务兼容,测试数据可重建,生产迁移策略后续上线前单独设计。`GET /api/reservation/orders` 可返回 V4 下一步订单任务、卡片、动作类型、动作状态、V4 open 数和统一展示字段 `open_work_item_count`;旧 `open_task_count` / `next_processable_task_id` 仅作历史诊断兼容。Room Information 已完成后端稳定展示模型和前端业务化展示:`GET /api/reservation/order-tasks/{orderTaskId}` 在 `display_payload.room_information` 返回 New / Update / Cancel 的 `current_values`、`proposed_values`、`final_values`、`change_summary[]`,前端只消费该展示模型和 `fields[]`,不再从 Agent raw payload、`business_fields` 或 `target_order` 自行推导;如果卡片 payload 已经是稳定 `room_information.final_values` 结构,后端会按稳定模型归一化查询和复核;Nights、Breakfast 和 Group Booking Status 均以后端派生值为准;确认和复核写入稳定 `confirmed_payload_json.room_information.final_values`,不回写 Agent 原始 `target_order`、Adult、邮件正文或附件 URL;接口对前端暴露的 `fields[].write_target` 使用 `confirmed_payload` / `review_resolution.field_overrides` 这类安全语义,不暴露内部列名;查询侧 `fields[].editable` 和命令侧 `review-resolution` 复核 pointer 校验已共用同一套 Room Information 字段策略。Trace 卡后端契约已收口:普通事项内容字段统一为 `trace_items[].text`,不使用 `content`;`department_code` 第一版只允许 `FO`、`HSK`、`FO+HSK`,任务详情字段会返回 `options_source=reservation_v4_trace_department_fixed` 和 `fixed_options[]` 三个固定选项;`EXTRA_BED.target_room_type_code` 只校验当前酒店 Room Type 目录存在,暂不校验当前订单已有房型;确认和复核共用同一套 Trace 字段白名单,`display_payload` / `confirmed_payload` 不返回 `target_order`、邮件正文、附件 URL、raw evidence 或 AI 原始 payload。复核 pointer 拒绝前会记录 `review_pointer_policy=m002_v4_review_pointer_runtime_fix_v1`,包含 order task、card、incoming pointer、query-side editable pointers、command-side allowed pointers、validation error pointers 和 reject reason,但不记录 payload、邮件正文或附件 URL。V4 任务详情 smoke 修复已完成:页面顺序固定为 Basic Information、业务卡、SourceMessage Display;来源邮件卡位于页面底部,只通过 SourceMessage conversation 接口定位当前触发邮件并默认折叠正文;Basic Information 和普通业务卡的展示 / 确认 payload 不再返回 Agent `target_order`,普通业务卡还会移除邮件 HTML、raw evidence、附件原始 URL 和 PMS 原始响应等敏感字段。Rooming List 卡确认时已实现 Group 自动置 `DEF`:如同订单存在可更新的已确认 Room Information 快照,后端会覆盖其 `group_booking_status=DEF` 并写 `V4_ROOMING_LIST_AUTO_DEF` 审计;刷新任务详情时 `display_payload` 和 `confirmed_payload` 均以 DEF 后的确认快照为准;当前订单详情 `order_overview` 不返回 Group Booking Status 字段;如没有可更新投影,Rooming List 确认仍成功,只写安全审计提示,不临时创建不完整 Room Information。V4 任务详情已支持 ROOMING_LIST 轻量事项卡:页面只显示 “Rooming List / 房表事项”、目标订单线索、人工处理说明和确认按钮,不展示 rows、名单明细、附件预览、导入 / 生成入口、AI payload、邮件正文或附件 URL;PENDING_CONFIRM 确认只提交 `version`,成功后完全使用后端刷新详情,不由前端自行设置 `group_booking_status=DEF`。OWNER RATE `RATECODE (2)` 已确认第一阶段 Room Type 稳定集合为 `RM2`、`RM3`、`RM4`、`SU1`、`SU2`、`SU3`,不建立 Account -> Room Type 关系;Rate Code 第一阶段暂不建立 Account 适用关系,Q.B.D 与 LIAN TAI 的 40 个规范化 Rate Code 仅作为当前酒店级 `RATE_CODE` 目录候选维护。Payment 卡已在 `display_payload.payment_attachments[]` 返回付款凭证附件安全摘要,字段只包含附件 ID、文件名、类型、大小、是否图片、是否理论可预览 / 下载和可选 `external_media_id`,前端在 V4 任务详情 Payment 卡中按当前触发 SourceMessage 的 conversation 附件匹配缩略图、大图预览和非图片下载,匹配优先 `external_media_id` / `externalMediaId`,其次 `attachment_id`,不按文件名猜测;`attachment_ids[]` 第一版仍只读,前端不增删或替换附件集合,Payment 确认只提交 `version`,不提交附件 ID、附件 URL 或完整附件对象;真实 URL 仍只来自 SourceMessage 原文权限链路,权限不足或 conversation 失败时降级展示不可预览 / 不可下载。已确认 `REVIEW_REQUIRED` 仍是原业务卡复核态,页面按钮统一叫“确认卡片”,复核态允许编辑当前卡 `fields[]` 白名单内业务字段,问题字段红字提示。后续可继续做测试机 V4 smoke 复测、OWNER RATE 目录导入、真实 PMS / OPERA / OHIP 同步或 SuperAgent 目录供给方案。 | +| 当前重点 | M002 V4 已停止普通业务入站双写旧 `workflow_reservation_task`,V4 后新业务主线只写 V4 order task / cards / source notification;Debug EML V4 smoke 默认复用实时 AgentBus V4 Open API subject,避免误走历史 Debug V2/V3 profile。开发阶段不维护 V2/V3 旧任务兼容,测试数据可重建,生产迁移策略后续上线前单独设计。`GET /api/reservation/orders` 可返回 V4 下一步订单任务、卡片、动作类型、动作状态、V4 open 数和统一展示字段 `open_work_item_count`;旧 `open_task_count` / `next_processable_task_id` 仅作历史诊断兼容。Room Information 已完成后端稳定展示模型和前端业务化展示:`GET /api/reservation/order-tasks/{orderTaskId}` 在 `display_payload.room_information` 返回 New / Update / Cancel 的 `current_values`、`proposed_values`、`final_values`、`change_summary[]`,前端只消费该展示模型和 `fields[]`,不再从 Agent raw payload、`business_fields` 或 `target_order` 自行推导;如果卡片 payload 已经是稳定 `room_information.final_values` 结构,后端会按稳定模型归一化查询和复核;Nights、Breakfast 和 Group Booking Status 均以后端派生值为准;确认和复核写入稳定 `confirmed_payload_json.room_information.final_values`,不回写 Agent 原始 `target_order`、Adult、邮件正文或附件 URL;接口对前端暴露的 `fields[].write_target` 使用 `confirmed_payload` / `review_resolution.field_overrides` 这类安全语义,不暴露内部列名;查询侧 `fields[].editable` 和命令侧 `review-resolution` 复核 pointer 校验已共用同一套 Room Information 字段策略。Trace 卡后端契约已收口:普通事项内容字段统一为 `trace_items[].text`,不使用 `content`;`department_code` 第一版只允许 `FO`、`HSK`、`FO+HSK`,任务详情字段会返回 `options_source=reservation_v4_trace_department_fixed` 和 `fixed_options[]` 三个固定选项;`EXTRA_BED.target_room_type_code` 只校验当前酒店 Room Type 目录存在,暂不校验当前订单已有房型;确认和复核共用同一套 Trace 字段白名单,`display_payload` / `confirmed_payload` 不返回 `target_order`、邮件正文、附件 URL、raw evidence 或 AI 原始 payload。复核 pointer 拒绝前会记录 `review_pointer_policy=m002_v4_review_pointer_runtime_fix_v1`,包含 order task、card、incoming pointer、query-side editable pointers、command-side allowed pointers、validation error pointers 和 reject reason,但不记录 payload、邮件正文或附件 URL。V4 任务详情 smoke 修复已完成:页面顺序固定为 Basic Information、业务卡、SourceMessage Display;来源邮件卡位于页面底部,只通过 SourceMessage conversation 接口定位当前触发邮件并默认折叠正文;Basic Information 和普通业务卡的展示 / 确认 payload 不再返回 Agent `target_order`,普通业务卡还会移除邮件 HTML、raw evidence、附件原始 URL 和 PMS 原始响应等敏感字段;Basic Information 的 Market Code / Source Code 前端已改为可编辑字段,普通业务页不再展示字段下方 control hint、lookup 空目录提示或“只读”胶囊。Rooming List 卡确认时已实现 Group 自动置 `DEF`:如同订单存在可更新的已确认 Room Information 快照,后端会覆盖其 `group_booking_status=DEF` 并写 `V4_ROOMING_LIST_AUTO_DEF` 审计;刷新任务详情时 `display_payload` 和 `confirmed_payload` 均以 DEF 后的确认快照为准;当前订单详情 `order_overview` 不返回 Group Booking Status 字段;如没有可更新投影,Rooming List 确认仍成功,只写安全审计提示,不临时创建不完整 Room Information。V4 任务详情已支持 ROOMING_LIST 轻量事项卡:页面只显示 “Rooming List / 房表事项”、目标订单线索、人工处理说明和确认按钮,不展示 rows、名单明细、附件预览、导入 / 生成入口、AI payload、邮件正文或附件 URL;PENDING_CONFIRM 确认只提交 `version`,成功后完全使用后端刷新详情,不由前端自行设置 `group_booking_status=DEF`。OWNER RATE `RATECODE (2)` 已确认第一阶段 Room Type 稳定集合为 `RM2`、`RM3`、`RM4`、`SU1`、`SU2`、`SU3`,不建立 Account -> Room Type 关系;Rate Code 第一阶段暂不建立 Account 适用关系,Q.B.D 与 LIAN TAI 的 40 个规范化 Rate Code 仅作为当前酒店级 `RATE_CODE` 目录候选维护。Payment 卡已在 `display_payload.payment_attachments[]` 返回付款凭证附件安全摘要,字段只包含附件 ID、文件名、类型、大小、是否图片、是否理论可预览 / 下载和可选 `external_media_id`,前端在 V4 任务详情 Payment 卡中按当前触发 SourceMessage 的 conversation 附件匹配缩略图、大图预览和非图片下载,匹配优先 `external_media_id` / `externalMediaId`,其次 `attachment_id`,不按文件名猜测;`attachment_ids[]` 第一版仍只读,前端不增删或替换附件集合,Payment 确认只提交 `version`,不提交附件 ID、附件 URL 或完整附件对象;真实 URL 仍只来自 SourceMessage 原文权限链路,权限不足或 conversation 失败时降级展示不可预览 / 不可下载。已确认 `REVIEW_REQUIRED` 仍是原业务卡复核态,页面按钮统一叫“确认卡片”,复核态允许编辑当前卡 `fields[]` 白名单内业务字段,问题字段红字提示。后续可继续做测试机 V4 smoke 复测、OWNER RATE 目录导入、真实 PMS / OPERA / OHIP 同步或 SuperAgent 目录供给方案。 | ## 1. 当前 Checkpoint @@ -37,7 +37,7 @@ - M010 Rooming List Excel 生成后端 CP1 和前端 V1 已实现:前端 `/reservation/rooming-lists/new` 上传来源名单和手工字段,后端同步生成 `.xlsx` 直接下载,第一版不落库、不上传 OSS。 - M011 Booking Excel 附件预处理 CP1/CP2/CP3 已实现:后端可排除人员名单类 Excel,按最近 6 个月候选窗口选择实际存在的最新 3 个业务月,抽取 Booking Update / 附加费表高亮行业务 JSON;Debug EML 和 AgentBus dispatch 在各自 include 开关与总开关同时启用时,会在调用 SuperAgent 前追加 `attachment_extractions[]`。测试机 AgentBus 增强已开启;生产链路仍默认关闭,生产开启需单独确认。 - M002 V4 CP1 当前已完成入站解析和现有任务链路过渡适配;M002 V4 CP2 已完成订单任务与多卡领域模型设计;M002 V4 CP3 已完成 V4 订单任务、多卡和 S10/S99 来源通知表结构与 Repository 基线;M002 V4 CP4 已完成入站写入新模型;M002 V4 CP5 已完成前端查询接口并补齐订单详情 `v4_order_tasks[]` 时间线;M002 V4 CP6 已完成普通卡片确认和 S10/S99 来源通知 ack;M002 V4 CP7 已完成 `REVIEW_REQUIRED` 卡复核解阻和复核场景订单归属确认;M002 V4 CP8 已完成目录校验、V4 卡片 `fields[]` 字段白名单、确认写入白名单收口和嵌套业务字段目录校验;M002 V4 CP11 已完成数据库目录、初始化种子、启动补种子、Account / Room Type / Rate Code lookup API,并把 V4 入站、确认、复核目录校验切换到当前酒店数据库目录;M002 V4 CP12 已完成前端 lookup 接入第一版和 V4 订单任务时间线消费;M002 V4 CP13 目录管理后台 CP1 已完成前后端列表、新增、启用 / 停用闭环;M002 V4 CP14 已完成订单列表 V4 继续处理入口字段和前端入口消费,`GET /api/reservation/orders` 返回 V4 下一步订单任务、卡片、动作类型、动作状态、V4 open 数和统一展示计数 `open_work_item_count`,前端按 V4 优先跳转,并按 `open_work_item_count` 展示待处理数量;V4 业务审计查询已补齐订单任务审计和来源通知 ack 审计两个只读接口;订单详情已补齐并完成前端接入 V4 `order_overview`、`next_v4_action`、`related_source_messages[]` 和 `v4_order_tasks[].cards[]`;V4 普通业务入站已停止双写旧 `workflow_reservation_task`;Room Information 后端展示模型和前端业务化展示第一版已完成;Rooming List 确认触发 Group Booking Status 自动置 `DEF` 已完成,前端轻量事项确认卡也已完成;Payment 附件安全摘要后端和前端预览接入均已完成。OWNER RATE Room Type / Rate Code 目录口径已落文档;真实 PMS 同步和 SuperAgent 目录机器接口仍未完成。 -- M002 V4 CP2 已确认:V4 工作台统一列表草案为 `/api/reservation/workbench-items`,业务订单任务接口新开 `/api/reservation/order-tasks/**`,S10/S99 来源通知详情草案为 `/api/reservation/source-notifications/{notificationId}`;S10/S99 使用来源通知模型,不再挂隐藏技术订单;`FIT + BOOKING_CODE` 不建 ACTIVE 唯一约束,匹配多条进人工复核;Basic Information 必须先确认;Rooming List 卡第一版只做事项确认;Account / Market / Source 当前通过数据库目录读取和派生;旧 V2/V3 任务详情和草稿确认接口后续可逐步废弃。 +- M002 V4 CP2 已确认:V4 工作台统一列表草案为 `/api/reservation/workbench-items`,业务订单任务接口新开 `/api/reservation/order-tasks/**`,S10/S99 来源通知详情草案为 `/api/reservation/source-notifications/{notificationId}`;S10/S99 使用来源通知模型,不再挂隐藏技术订单;`FIT + BOOKING_CODE` 不建 ACTIVE 唯一约束,匹配多条进人工复核;Basic Information 必须先确认;Rooming List 卡第一版只做事项确认;Account 通过数据库目录选择,Market / Source 可默认来自目录并允许前端人工覆盖提交;旧 V2/V3 任务详情和草稿确认接口后续可逐步废弃。 ## 5. Next Steps diff --git a/client/src/components/reservation/ReservationV4TaskCardFieldRenderer.vue b/client/src/components/reservation/ReservationV4TaskCardFieldRenderer.vue index f665dd6..8d63219 100644 --- a/client/src/components/reservation/ReservationV4TaskCardFieldRenderer.vue +++ b/client/src/components/reservation/ReservationV4TaskCardFieldRenderer.vue @@ -7,9 +7,8 @@ :class="{ 'v4-field--readonly': !isEditable(field) }" > - {{ field.display_name }} + {{ fieldDisplayName(field) }} * - {{ t('taskV4.field.readonly') }} {{ field.control_hint }} - {{ hint }} + v-if="lookupError(field)" + class="trace-field__hint trace-field__hint--error" + >{{ lookupError(field) }} { await wrapper.find('select').setValue('ACC-LIVE') await flushPromises() - expect(wrapper.text()).toContain('LEISURE') - expect(wrapper.text()).toContain('TRAVEL_AGENT') + const basicCard = wrapper.findAll('.task-card-section')[0]! + expect(basicCard.find('.v4-field__label em').exists()).toBe(false) + expect(basicCard.text()).not.toContain('Market 由 Account Code 派生') + expect(basicCard.text()).not.toContain('Source 由 Account Code 派生') + expect(basicCard.text()).not.toContain('选择后会带出市场') + const marketField = findFieldByLabel(basicCard, '市场代码') + const sourceField = findFieldByLabel(basicCard, '来源代码') + expect(marketField?.find('input[name="/basic_information/market_code"]').exists()).toBe(true) + expect(sourceField?.find('input[name="/basic_information/source_code"]').exists()).toBe(true) + expect((marketField?.find('input[name="/basic_information/market_code"]').element as HTMLInputElement).value) + .toBe('LEISURE') + expect((sourceField?.find('input[name="/basic_information/source_code"]').element as HTMLInputElement).value) + .toBe('TRAVEL_AGENT') + await marketField?.find('input[name="/basic_information/market_code"]').setValue('MICE') + await sourceField?.find('input[name="/basic_information/source_code"]').setValue('DIRECT') await wrapper.findAll('button').find((button) => button.text().includes('确认卡片'))?.trigger('click') await flushPromises() @@ -125,6 +138,8 @@ describe('reservation V4 pages', () => { confirmed_payload: { basic_information: { account_code: 'ACC-LIVE', + market_code: 'MICE', + source_code: 'DIRECT', }, }, }) @@ -830,6 +845,9 @@ describe('reservation V4 pages', () => { expect(roomCard.text()).not.toContain('target_order') expect(roomCard.text()).not.toContain('Adult') expect(roomCard.text()).not.toContain('Legacy Room Type') + expect(roomCard.text()).not.toContain('Group Block Name') + expect(roomCard.text()).not.toContain('Rate Code') + expect(roomCard.text()).not.toContain('Group Booking Status') expect(roomCard.find('input[name="/room_information/final_values/adult"]').exists()).toBe(false) expect(roomCard.find('input[name="/room_information/final_values/nights"]').exists()).toBe(false) expect(roomCard.find('input[name="/room_information/final_values/target_order/locator_value"]').exists()).toBe(false) @@ -1194,7 +1212,7 @@ describe('reservation V4 pages', () => { ) await flushPromises() - expect(wrapper.text()).toContain(zhCN.taskV4.lookup.empty) + expect(wrapper.text()).not.toContain(zhCN.taskV4.lookup.empty) expect(wrapper.text()).not.toContain(zhCN.taskV4.lookup.stale) expect(wrapper.text()).not.toContain('PMS sync is stale') expect(wrapper.text()).not.toContain('PMS_SYNC') @@ -1307,9 +1325,6 @@ describe('reservation V4 pages', () => { page_num: 1, page_size: 20, }) - expect(wrapper.text()).toContain('MICE') - expect(wrapper.text()).toContain('DIRECT') - await wrapper.findAll('button').find((button) => button.text().includes('确认卡片'))?.trigger('click') await flushPromises() @@ -1318,6 +1333,8 @@ describe('reservation V4 pages', () => { confirmed_payload: { basic_information: { account_code: 'ACC-101', + market_code: 'LEISURE', + source_code: 'TRAVEL_AGENT', }, }, }) @@ -1347,7 +1364,7 @@ describe('reservation V4 pages', () => { ) await flushPromises() - expect(wrapper.text()).toContain(zhCN.taskV4.lookup.empty) + expect(wrapper.text()).not.toContain(zhCN.taskV4.lookup.empty) expect(wrapper.text()).not.toContain('固定种子初始化') expect(wrapper.text()).not.toContain('真实 PMS') expect(wrapper.text()).not.toContain('PMS_SYNC') @@ -1582,6 +1599,8 @@ function createOrderTaskDetail(options: { businessFields?: ReservationV4TaskCardResult['fields'] sourceDisplayPayload?: ReservationV4TaskCardResult['display_payload'] basicAccountValue?: string + basicMarketCodeValue?: string + basicSourceCodeValue?: string basicAccountRequired?: boolean } = {}): ReservationV4OrderTaskDetailResult { const sourceCard = createCard('card-source', 'SOURCE_MESSAGE_DISPLAY', 'READONLY', { @@ -1609,6 +1628,26 @@ function createOrderTaskDetail(options: { options_source: 'RESERVATION_V4_ACCOUNT_CATALOG', control_type: 'SELECT', }), + createField('/basic_information/market_code', { + display_name: 'Market Code', + value: options.basicMarketCodeValue ?? 'LEISURE', + editable: false, + raw_readonly: true, + control_type: 'READONLY', + edit_scope: 'NEVER', + write_target: 'NONE', + control_hint: 'Market 由 Account Code 派生,前端只读展示。', + }), + createField('/basic_information/source_code', { + display_name: 'Source Code', + value: options.basicSourceCodeValue ?? 'TRAVEL_AGENT', + editable: false, + raw_readonly: true, + control_type: 'READONLY', + edit_scope: 'NEVER', + write_target: 'NONE', + control_hint: 'Source 由 Account Code 派生,前端只读展示。', + }), createField('/basic_information/read_only_marker', { display_name: 'Read only marker', value: 'VISIBLE', diff --git a/client/src/views/reservation/ReservationV4OrderTaskDetailView.vue b/client/src/views/reservation/ReservationV4OrderTaskDetailView.vue index df745f8..a95b806 100644 --- a/client/src/views/reservation/ReservationV4OrderTaskDetailView.vue +++ b/client/src/views/reservation/ReservationV4OrderTaskDetailView.vue @@ -283,11 +283,12 @@ async function loadDetail(): Promise { } function applyDetail(result: ReservationV4OrderTaskDetailResult): void { - detail.value = result + const normalizedResult = normalizeReservationV4Detail(result) + detail.value = normalizedResult const cards = [ - result.source_message_card, - result.basic_information_card, - ...result.business_cards, + normalizedResult.source_message_card, + normalizedResult.basic_information_card, + ...normalizedResult.business_cards, ].filter((card): card is ReservationV4TaskCardResult => Boolean(card)) cardValues.value = cards.reduce>((values, card) => { values[card.card_id] = buildReservationV4InitialFieldValues(card.fields) @@ -298,13 +299,60 @@ function applyDetail(result: ReservationV4OrderTaskDetailResult): void { cardSuccessMessages.value = {} reviewForms.value = cards.reduce>((forms, card) => { forms[card.card_id] = { - confirmed_order_id: result.order_task.order_id ?? '', + confirmed_order_id: normalizedResult.order_task.order_id ?? '', reason: '', } return forms }, {}) } +function normalizeReservationV4Detail(result: ReservationV4OrderTaskDetailResult): ReservationV4OrderTaskDetailResult { + return { + ...result, + basic_information_card: result.basic_information_card + ? normalizeBasicInformationCard(result.basic_information_card) + : result.basic_information_card, + } +} + +function normalizeBasicInformationCard(card: ReservationV4TaskCardResult): ReservationV4TaskCardResult { + return { + ...card, + fields: card.fields.map((field) => + isEditableBasicMarketSourceField(field) + ? normalizeEditableBasicMarketSourceField(card, field) + : field, + ), + } +} + +function normalizeEditableBasicMarketSourceField( + card: ReservationV4TaskCardResult, + field: ReservationV4TaskCardResult['fields'][number], +): ReservationV4TaskCardResult['fields'][number] { + const reviewMode = card.card_status === 'REVIEW_REQUIRED' + return { + ...field, + editable: true, + raw_readonly: false, + control_type: 'TEXT', + edit_scope: reviewMode ? 'REVIEW' : 'CONFIRM', + write_target: reviewMode ? 'REVIEW_RESOLUTION_FIELD_OVERRIDES' : 'CONFIRMED_PAYLOAD_JSON', + control_hint: null, + } +} + +function isEditableBasicMarketSourceField(field: ReservationV4TaskCardResult['fields'][number]): boolean { + const key = normalizeV4FieldKey(field) + return key === '/basic_information/market_code' || key === '/basic_information/source_code' +} + +function normalizeV4FieldKey(field: ReservationV4TaskCardResult['fields'][number]): string { + return reservationV4FieldKey(field) + .replace(/\./g, '/') + .replace(/^([^/])/, '/$1') +} + function setCardValues(card: ReservationV4TaskCardResult, values: ReservationRecord): void { cardValues.value = { ...cardValues.value, diff --git a/docs/project/frontend-backend/backend-to-frontend-notes.md b/docs/project/frontend-backend/backend-to-frontend-notes.md index 3578cf5..f869c39 100644 --- a/docs/project/frontend-backend/backend-to-frontend-notes.md +++ b/docs/project/frontend-backend/backend-to-frontend-notes.md @@ -60,7 +60,7 @@ | `GET /api/reservation/order-tasks/{orderTaskId}/audits` | 查询 V4 订单任务审计流水 | 必须带 Bearer token,需要 `RESERVATION_AUDIT_READ`,后端按订单任务实际酒店校验访问权;返回 `order_task_id` 和 `items[]`。`items[]` 用于展示 V4 卡片确认、复核解阻、订单归属确认轨迹和 `V4_ROOMING_LIST_AUTO_DEF` 自动 DEF 摘要,只包含脱敏后的审计摘要,不包含邮件正文、HTML、附件 URL、AI 原始 payload、token 或 secret。 | | `GET /api/reservation/source-notifications/{notificationId}` | 查询 V4 S10/S99 来源通知详情 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`,后端按来源通知实际酒店校验访问权;只返回通知摘要、来源邮件通知卡、会话摘要和 `availability`;不返回订单任务、业务卡、邮件正文、附件 URL 或原始 AI payload。 | | `GET /api/reservation/source-notifications/{notificationId}/audits` | 查询 V4 S10/S99 来源通知审计流水 | 必须带 Bearer token,需要 `RESERVATION_AUDIT_READ`,后端按来源通知实际酒店校验访问权;返回 `notification_id` 和 `items[]`。`items[]` 第一版用于展示来源通知 ack 记录,只包含脱敏后的审计摘要。 | -| `GET /api/reservation/lookups/accounts` | 查询 V4 Account 目录 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`,支持 `hotel_id`、`keyword`、`page_num`、`page_size`;返回统一 wrapper:`hotel_id`、`catalog_type=ACCOUNT`、`catalog_source`、`catalog_version`、`stale`、`items[]`、`page`、`warnings[]`。`keyword` 匹配目录 code 时后端按稳定 code 大写归一化处理,前端可传小写;匹配显示名仍按数据库比较规则。`keyword` 无匹配时 `items=[]` / `page.total=0`,但只要酒店未过滤目录存在,`catalog_source/catalog_version` 仍保持真实目录元数据,不代表目录未初始化。前端在 `options_source=reservation_v4_account_catalog` 时调用,只提交 `items[].code`,Market / Source 以后端确认派生结果为准。 | +| `GET /api/reservation/lookups/accounts` | 查询 V4 Account 目录 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`,支持 `hotel_id`、`keyword`、`page_num`、`page_size`;返回统一 wrapper:`hotel_id`、`catalog_type=ACCOUNT`、`catalog_source`、`catalog_version`、`stale`、`items[]`、`page`、`warnings[]`。`keyword` 匹配目录 code 时后端按稳定 code 大写归一化处理,前端可传小写;匹配显示名仍按数据库比较规则。`keyword` 无匹配时 `items=[]` / `page.total=0`,但只要酒店未过滤目录存在,`catalog_source/catalog_version` 仍保持真实目录元数据,不代表目录未初始化。前端在 `options_source=reservation_v4_account_catalog` 时调用;Account 字段只提交 `items[].code`,Market / Source 可作为 Basic Information 字段默认值展示并允许用户覆盖。 | | `GET /api/reservation/lookups/room-types` | 查询 V4 Room Type 目录 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`,支持 `hotel_id`、`keyword`、`page_num`、`page_size`;第一版只返回当前酒店 `ACTIVE` 房型目录,不接日期过滤,不代表 PMS 全量房型。M002-V4-owner-rate-catalog-data-alignment 后固定初始化目录已收敛为 `RM2`、`RM3`、`RM4`、`SU1`、`SU2`、`SU3` 六个 code。`keyword` 匹配房型 code 时后端按稳定 code 大写归一化处理,前端可传小写;匹配显示名仍按数据库比较规则。`keyword` 无匹配时按空选项处理,不要当作目录不可用。前端在 `options_source=reservation_v4_room_type_catalog` 时调用。 | | `GET /api/reservation/lookups/rate-codes` | 查询 V4 Rate Code 目录 | 当前 CP11 实现支持 `hotel_id`、`keyword`、`page_num`、`page_size`,返回当前酒店 `ACTIVE` Rate Code;M002-V4-owner-rate-catalog-data-alignment 后固定初始化目录已收敛为 OWNER RATE `RATECODE (2)` 的 40 个规范化酒店级 code。2026-07-21 结论是第一阶段暂不建立 Account 与 Rate Code 的适用关系,Rate Code lookup 继续按酒店级目录返回,不要求 `account_code` / `booking_type`。`pricing_available=false` 表示后端未接真实价格,不要据此展示价格。`keyword` 匹配 Rate Code 时后端按稳定 code 大写归一化处理,前端可传小写;匹配显示名仍按数据库比较规则。前端在 `options_source=reservation_v4_rate_code_catalog` 时调用。 | | `GET /api/admin/reservation/catalogs/accounts` | 管理后台 Account 目录列表 | 必须带 Bearer token,需要 `RESERVATION_CATALOG_MANAGE` 和目标酒店访问权;支持 `hotel_id`、`keyword`、`status=ACTIVE/DISABLED`、`page_num`、`page_size`;`keyword` 搜索 code 时大小写不敏感,显示名仍按数据库比较规则;返回 `items[] + page`,包含 `id`、`account_code`、`account_name`、`market_code`、`source_code`、`status`、`catalog_source`、`external_account_id`、`catalog_version`、`metadata_json`、`version`、`created_at`、`updated_at`。 | @@ -111,7 +111,7 @@ | `GET /api/source-messages/{sourceMessageId}/conversation` | 新增邮件会话详情接口,并补齐 `html_body_sanitized` / `html_render_mode`。 | 当前唯一推荐路径是这个接口;前端渲染邮件 HTML 时优先使用 `html_body_sanitized`;不要调用历史讨论过的 `/api/source-message-conversations/{externalConversationId}`。 | | `POST /api/system/debug/eml-superagent-runs` | 新增 Debug EML 上传到 SuperAgent 调试接口,并补齐独立 Debug 外部消息 ID、原始 Message-ID 保留、安全 HTML 字段和入口通知识别。 | 只用于调试页面;请求为 multipart/form-data;必须传 `X-TH-Hotel-Debug-Upload-Key`,但该 key 不能写进前端源码、构建产物、URL、localStorage 或错误上报;SuperAgent 返回旧 S000/S999 或新 S10/S99 入口通知时都不应被前端视为 JSON 解析失败。测试机 V4 smoke 默认使用实时 AgentBus V4 subject,历史 Debug V2/V3 profile 只能由后端显式配置,不应作为 V4 smoke 入口。 | | `GET /api/reservation/workbench-items` / `/api/reservation/order-tasks/**` / `/api/reservation/source-notifications/{notificationId}` | 新增 M002 V4 CP5 查询接口,并在 CP6 打开卡片确认 / 来源通知 ack availability。 | 这是 V4 新模型前端主入口;前端应按每张卡或通知返回的 `availability.confirmable`、`availability.ackable`、`readonly_reason_code` 控制按钮。工作台条目已返回 `created_at` / `updated_at` 作为排序兜底和调试字段;前端不要继续从旧 `/api/reservation/tasks/**` 推断 V4 多卡详情。 | -| `GET /api/reservation/lookups/accounts` / `/room-types` / `/rate-codes` | 新增 M002 V4 CP11 数据库目录 lookup。 | 前端从任务详情 `fields[].options_source` 选择调用哪个 lookup;只提交返回项的 `code`,不要提交显示名、派生 Market / Source、目录完整对象或前端自造 code。2026-07-21 结论是 Rate Code 第一阶段仍按酒店级目录 lookup,不做 Account 过滤;未来如新增适用关系,再另行扩展联动参数。`warnings[]` 非空时可做非阻塞提示。 | +| `GET /api/reservation/lookups/accounts` / `/room-types` / `/rate-codes` | 新增 M002 V4 CP11 数据库目录 lookup。 | 前端从任务详情 `fields[].options_source` 选择调用哪个 lookup;lookup 字段只提交返回项的 `code`,不要提交显示名、目录完整对象或前端自造 code。Basic Information 中 `market_code` / `source_code` 可由 Account 目录默认带出,但前端按可编辑字段允许人工覆盖,并随 `basic_information.market_code` / `basic_information.source_code` 提交,最终以后端确认校验和落库为准。2026-07-21 结论是 Rate Code 第一阶段仍按酒店级目录 lookup,不做 Account 过滤;未来如新增适用关系,再另行扩展联动参数。`warnings[]` 非空时默认不在普通业务页展示,如需保留应进入技术折叠区或调试模式。 | | `GET/POST/PUT /api/admin/reservation/catalogs/...` | 新增 M002 V4 目录管理后台 CP1 后端接口。 | 仅供系统设置 / 管理后台页面使用,必须带 `RESERVATION_CATALOG_MANAGE`;支持 Account、Room Type、Rate Code 列表、新增、启用 / 停用;停用后普通 lookup 不再返回该目录项。 | V4 CP5 分页注意:`page_num` 从 1 开始,后端第一版安全上限为 100;`page_size` 最大 100。超出上限时后端按上限处理并在 `page.page_num` / `page.page_size` 中返回实际使用值。`order_task_status` 和 `card_status` 是稳定枚举查询参数,前端不要传中文文案或自造状态码。 @@ -553,9 +553,9 @@ RESERVATION_ROOMING_LIST_GENERATE - CP8 / Room Information 展示模型后,确认接口按 `fields[]` 白名单收口:前端可以只提交用户修改过的可编辑字段,不建议整包回传 `display_payload`。后端会从当前卡展示快照生成确认快照,并只合并可写叶子字段;来源邮件、路由、`target_order`、`order_ref`、`manual_review`、校验诊断字段以及前端额外注入字段不会写入内部确认快照。 - 业务卡目录校验会递归检查稳定模型或历史兼容结构。例如 Room Information 新结构的房型位于 `/room_information/final_values/room_items/0/room_type_code`,错误详情会使用 `room_information.final_values.room_items.0.room_type_code`;历史兼容 `UPDATE_BOOKING` 的房型可能仍使用 `business_fields.after.room_items.0.room_type_code`。前端展示错误时优先用 `fields[].validation_errors`,接口 400 时可直接展示 `details[]`。 - `review-resolution` 请求示例:`{"version":0,"reason":"确认房型映射","confirmed_order_id":"123456","field_overrides":[{"field_pointer":"/room_information/final_values/room_items/0/room_type_code","value":"RM2"}]}`。`confirmed_order_id` 在订单任务归属未解决时必填;如果订单任务已经绑定订单且 `target_resolution_status=RESOLVED`,只能不传或传当前同一个订单 ID,不能借该接口切换到其它订单。`field_pointer` 必须来自当前卡 `fields[]` 中可编辑的 `basic_information.*`、`room_information.final_values.*` 或历史兼容 `business_fields.*` 叶子字段;复核态允许编辑当前卡业务白名单内字段,不再限定只能改空值、`missing_fields[]` 或目录错误字段。前端不要提交来源邮件、路由、`target_order`、`order_ref`、缺失字段清单、`manual_review`、raw evidence、校验诊断字段,也不能替换整个对象 / 数组。 -- V4 `fields[]` 第一版字段说明:Basic Information 固定返回 `/basic_information/account_code`、`/basic_information/market_code`、`/basic_information/source_code`;其中 Account `control_type=select`、`options_source=reservation_v4_account_catalog`,Market / Source 为只读派生字段。Room Information 字段统一返回 `/room_information/final_values/...`,例如 `/room_information/final_values/arrival_date`、`/room_information/final_values/room_items/0/room_type_code`;`REVIEW_REQUIRED` 状态下只要字段仍在当前卡业务白名单内且未被前置阻塞,就会返回 `editable=true` 并允许 `review-resolution` 提交同一个 pointer;即使该叶子字段原始值缺失、详情页显示 `value=null`,前端仍可按原样提交该 pointer。查询侧 editable 计算和命令侧 pointer 校验共用同一套 Room Information 字段策略。测试机如仍出现 `V4_REVIEW_POINTER_NOT_ALLOWED`,确认部署后让后端日志检索 `review_pointer_policy=m002_v4_review_pointer_runtime_fix_v1`,日志会输出两侧 pointer 白名单、validation error pointers、`display_payload_has_room_information_final_values` 和拒绝原因。前端不要自行补未返回字段。 -- V4 CP11 已开放独立目录 lookup API。前端应使用 `GET /api/reservation/lookups/accounts`、`GET /api/reservation/lookups/room-types`、`GET /api/reservation/lookups/rate-codes` 渲染 Account / Room Type / Rate Code 选项;用户提交确认或复核时只提交稳定 `code`,不要提交显示名、派生 Market / Source 或目录完整对象;后端确认前仍会重新校验目录。2026-07-21 结论是 Rate Code 第一阶段继续按当前酒店级目录查询,不依赖 Account + `booking_type` 过滤,也不硬编码 OWNER RATE Excel 中的 Account 映射;未来如新增适用关系,再由后端接口提供联动能力。`keyword` 查不到只表示当前筛选无结果,不能仅凭 `items=[]` 判断目录未初始化,应结合 `catalog_source`、`catalog_version` 和 `warnings[]`。 -- M002 V4 CP12 前端已接入上述三个 lookup API:V4 多卡详情页会按当前卡 `fields[].options_source` 拉取目录选项,空 `items[]`、`stale=true` 和 `warnings[]` 作为非阻塞提示展示;Account 选择后只展示目录返回的 `market_code` / `source_code` 辅助确认,确认 / 复核请求仍只提交用户选择的 code。 +- V4 `fields[]` 第一版字段说明:Basic Information 固定返回 `/basic_information/account_code`、`/basic_information/market_code`、`/basic_information/source_code`;其中 Account `control_type=select`、`options_source=reservation_v4_account_catalog`,Market / Source 前端按可编辑文本字段展示,默认值可来自 Account 目录派生或后端当前 value,用户可覆盖;确认 / 复核时随 `basic_information.market_code` / `basic_information.source_code` 提交,后端最终校验和落库为准。Room Information 字段统一返回 `/room_information/final_values/...`,例如 `/room_information/final_values/arrival_date`、`/room_information/final_values/room_items/0/room_type_code`;`REVIEW_REQUIRED` 状态下只要字段仍在当前卡业务白名单内且未被前置阻塞,就会返回 `editable=true` 并允许 `review-resolution` 提交同一个 pointer;即使该叶子字段原始值缺失、详情页显示 `value=null`,前端仍可按原样提交该 pointer。查询侧 editable 计算和命令侧 pointer 校验共用同一套 Room Information 字段策略。测试机如仍出现 `V4_REVIEW_POINTER_NOT_ALLOWED`,确认部署后让后端日志检索 `review_pointer_policy=m002_v4_review_pointer_runtime_fix_v1`,日志会输出两侧 pointer 白名单、validation error pointers、`display_payload_has_room_information_final_values` 和拒绝原因。前端不要自行补未返回字段。 +- V4 CP11 已开放独立目录 lookup API。前端应使用 `GET /api/reservation/lookups/accounts`、`GET /api/reservation/lookups/room-types`、`GET /api/reservation/lookups/rate-codes` 渲染 Account / Room Type / Rate Code 选项;用户提交确认或复核时 lookup 字段只提交稳定 `code`,不要提交显示名或目录完整对象;Basic Information 的 Market / Source 作为用户可覆盖业务字段提交。后端确认前仍会重新校验目录。2026-07-21 结论是 Rate Code 第一阶段继续按当前酒店级目录查询,不依赖 Account + `booking_type` 过滤,也不硬编码 OWNER RATE Excel 中的 Account 映射;未来如新增适用关系,再由后端接口提供联动能力。`keyword` 查不到只表示当前筛选无结果,不能仅凭 `items=[]` 判断目录未初始化,应结合 `catalog_source`、`catalog_version` 和 `warnings[]`。 +- M002 V4 CP12 前端已接入上述三个 lookup API:V4 多卡详情页会按当前卡 `fields[].options_source` 拉取目录选项;普通业务页默认不展示空目录、`stale=true` 或 `warnings[]` 等技术提示,接口失败和当前值不在目录中仍展示错误;Account 选择后的 Market / Source 默认值可用于人工确认,最终用户可在 Basic Information 中手工覆盖并提交。 - V4 新模型确认口径是不保存后端草稿、卡片最终确认后锁定、技术异常不进入用户可处理卡、当前不生成 OPERA 模拟操作。Basic Information 必须先确认;其它业务卡第一版不强制逐张顺序确认。现有 V3 `draft`、`confirm`、`manual-review-resolutions` 和 OPERA 模拟接口仍只代表旧链路能力,不能直接等同 V4 多卡最终接口。 - V4 S10/S99 已采用来源通知模型入库:新 V4 `route_code=S10/S99` 不再挂隐藏技术订单,也不再创建旧 `SOURCE_MESSAGE_ONLY` 任务;对应工作台 / 来源通知详情查询接口和 ack 写接口已开放。旧 `SOURCE_MESSAGE_ONLY` 只读任务仅代表 V3 S10/S99 和旧 S000/S999 兼容数据。 - M002 V3 的结构化 `S10/S99` 入站、40 条 P0.1 路由枚举 / 稳定配置、`UNHANDLED_CURRENT_INTENT`、`adapter_contract_error` transition 最小落库、任务列表 / 订单时间线 / 任务详情 V3 路由字段和只读诊断块透出、type-known manual review 同卡解阻第一版、typed infrastructure error、P0 fixtures 回归基线和 Parent Group / Cancel Allotment 路由修订均已完成。