diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index bad9b45..83f7889 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -5,14 +5,15 @@ | 最近更新 | 2026-07-20 | | 当前分支 | `feature/huangting` | | 当前阶段 | M002 V4 入站、多卡模型、持久化基线、入站写入、查询接口、卡片确认、复核解阻、目录校验、订单详情 V4 总览、DB 目录、Lookup API、前端 lookup 接入、目录管理后台 CP1 前后端、订单列表 V4 继续处理入口 / open count 收口和 V4 业务审计查询 | -| 当前重点 | M002 V4 已完成订单列表 V4 继续处理入口字段和前端消费,`GET /api/reservation/orders` 可返回 V4 下一步订单任务、卡片、动作类型、动作状态、V4 open 数和统一展示字段 `open_work_item_count`;`GET /api/reservation/orders/{orderId}` 已补齐订单详情 `order_overview`、`next_v4_action`、`related_source_messages[]` 和 `v4_order_tasks[].cards[]`,支撑订单总览页;V4 订单任务和 S10/S99 来源通知已补齐业务审计查询接口;后续可做前端订单详情 V4 化、测试机联调、真实 PMS / OPERA / OHIP 同步或 SuperAgent 目录供给方案 | +| 当前重点 | M002 V4 已完成订单列表 V4 继续处理入口字段和前端消费,`GET /api/reservation/orders` 可返回 V4 下一步订单任务、卡片、动作类型、动作状态、V4 open 数和统一展示字段 `open_work_item_count`;`GET /api/reservation/orders/{orderId}` 已补齐订单详情 `order_overview`、`next_v4_action`、`related_source_messages[]` 和 `v4_order_tasks[].cards[]`,前端订单详情页已改为 V4 订单总览页;V4 订单任务和 S10/S99 来源通知已补齐业务审计查询接口;2026-07-20 测试机 smoke 已完成主链路验证,但测试机订单详情响应疑似未部署到包含 CP15.1 的最新后端包,需要重新部署后复测订单详情 V4 总览;后续可做真实 PMS / OPERA / OHIP 同步或 SuperAgent 目录供给方案 | ## 1. 当前 Checkpoint -- 名称:`M002-V4-order-detail-overview-backend` -- 状态:Done,订单详情接口已补齐 V4 订单总览数据:`GET /api/reservation/orders/{orderId}` 返回 `order_overview`、`next_v4_action`、`related_source_messages[]` 和 `v4_order_tasks[].cards[]`。`order_overview` 只从已确认 V4 卡片派生,未确认 AI 建议不作为订单事实;`next_v4_action` 复用订单列表下一步处理口径,前端仍跳 V4 订单任务详情处理。 -- 目标:支撑前端把订单详情页从旧任务处理页改成“订单总览 + 当前确认快照 + V4 任务时间线 + 下一步入口”。 -- 边界:本轮不改前端、不新增表、不改 V4 写操作、不接真实 OPERA / OHIP、不废弃旧 V2/V3 详情接口。 +- 名称:`M002-V4-order-detail-overview-frontend` +- 状态:Done,前端订单详情页已改为“订单总览页”:展示 `order_overview` 当前确认快照、`next_v4_action` 下一步入口、`related_source_messages[]` 关联来源消息摘要,以及 `v4_order_tasks[].cards[]` V4 订单任务和任务卡安全摘要。点击下一步入口或 V4 时间线任务进入 `/reservation/order-tasks/{orderTaskId}`。 +- 目标:让订单详情页聚焦订单视角总览,不在该页直接确认、复核或编辑 V4 任务卡。 +- 边界:本轮不改后端、不改 V4 写操作、不展示 `ai_payload_json`、`display_payload_json`、`confirmed_payload_json`、邮件正文或附件 URL;旧 V2/V3 详情接口和独立任务详情页继续保留。 +- 联调备注:2026-07-20 测试机 V4 主链路 smoke 已完成登录 / 权限 / 酒店隔离、V4 工作台、V4 订单任务列表和详情、lookup、Basic Information 确认、业务卡确认、S10/S99 来源通知详情和 ack、V4 业务审计验证;当前测试机没有待复核 `REVIEW_REQUIRED` 卡,复核写操作未覆盖。测试机 `GET /api/reservation/orders/{orderId}` 仍返回旧响应形态,缺少 CP15.1 的 `order_overview`、`next_v4_action`、`related_source_messages[]` 和 `v4_order_tasks[].cards[]`,优先排查测试机是否部署了包含订单详情 V4 总览的最新包。 ## 2. 当前优先级 @@ -35,12 +36,12 @@ - 后续每完成一个 Feature 或 Checkpoint,需要更新本文件,避免项目状态继续沉淀在聊天记录里。 - 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[]`。真实 PMS 同步和 SuperAgent 目录机器接口仍未完成。 +- 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[]`。真实 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 必须先确认;Account / Market / Source 当前通过数据库目录读取和派生;旧 V2/V3 任务详情和草稿确认接口后续可逐步废弃。 ## 5. Next Steps -- 后续如继续做 M002 V4,可优先让前端改造订单详情 V4 总览页并进行测试机联调,或推进真实 PMS / OPERA / OHIP 目录同步和 `workflow_reservation_catalog_sync_run` checkpoint。 +- 后续如继续做 M002 V4,可优先进行测试机联调,或推进真实 PMS / OPERA / OHIP 目录同步和 `workflow_reservation_catalog_sync_run` checkpoint。 - 后续新增重要功能时,优先在 `docs/project/requirements/` 或未来 `docs/specs/` 中形成 Spec,再实现代码。 - M010 后续如需预览、历史记录、OSS 下载、订单 / 任务预填或客户字段目录化,再单独开前后端 checkpoint。 - M011 CP4 暂不推进;当前停留在 CP3 边界,只增强 SuperAgent 输入,不直接落订单、任务或长期解析历史。后续如确实需要运营查询或长期追踪,再单独设计 Excel 解析批次 / 行级持久化表。 diff --git a/client/src/i18n/locales/en-US.ts b/client/src/i18n/locales/en-US.ts index 50fb3f2..e892be3 100644 --- a/client/src/i18n/locales/en-US.ts +++ b/client/src/i18n/locales/en-US.ts @@ -454,6 +454,43 @@ export default { taskQueue: 'Order task queue', v4TaskTimeline: 'V4 order task timeline', v4TaskTimelineEmpty: 'No V4 order tasks.', + v4TaskTimelineHint: 'Shows safe order-task and card summaries only. Complete actions in the V4 task detail page.', + v4OverviewTitle: 'Current V4 confirmed snapshot', + v4OverviewSubtitle: 'Derived from confirmed cards only, not from unconfirmed AI suggestions.', + v4OverviewEmpty: 'No confirmed V4 order snapshot yet.', + accountCode: 'Account Code', + accountName: 'Account Name', + marketCode: 'Market Code', + sourceCode: 'Source Code', + departureDate: 'Departure date', + rateCode: 'Rate Code', + roomItems: 'Confirmed room items', + latestConfirmedAt: 'Latest confirmed at', + traceCardStatus: 'Trace card status', + roomingListCardStatus: 'Rooming List card status', + paymentCardStatus: 'Payment card status', + nextV4ActionTitle: 'Next action', + nextV4ActionSubtitle: 'The order overview only links to the task page; it does not confirm, review, or edit cards.', + nextV4ActionType: 'Action type', + nextV4ActionStatus: 'Action status', + nextV4CardId: 'Card ID', + openOrderTaskCount: 'Open V4 tasks', + enterV4Task: 'Open task page', + nextActionConfirm: 'Confirm card', + nextActionReview: 'Manual review', + nextActionNone: 'No pending action', + nextV4ActionEmpty: 'No next V4 action.', + relatedSourceMessages: 'Related source messages', + relatedSourceMessagesHint: 'Shows headers and conversation summary only. Open the conversation page for full email content.', + relatedSourceMessagesEmpty: 'No related source messages.', + externalConversationId: 'External conversation ID', + latestActivityAt: 'Latest activity', + v4CardsEmpty: 'No card summaries.', + cardId: 'Card ID', + reviewStatus: 'Review status', + confirmedBy: 'Confirmed by', + confirmedAt: 'Confirmed at', + sourceEventIndex: 'Source event {index}', sourceMessage: 'Source message', businessKeySource: 'Business key source', updatedAt: 'Last updated', @@ -822,6 +859,12 @@ export default { INFORMATIONAL_MESSAGE: 'Message notification (legacy)', SOURCE_MESSAGE_REVIEW_NOTIFICATION: 'Source message notification', SOURCE_MESSAGE_ONLY: 'Source message only', + SOURCE_MESSAGE_DISPLAY: 'Source message display card', + SOURCE_MESSAGE_NOTIFICATION: 'Source message notification card', + BASIC_INFORMATION: 'Basic Information card', + ROOM_INFORMATION: 'Room Information card', + TRACE_RESERVATION_NOTES: 'Trace notes card', + PAYMENT: 'Payment card', MANUAL_REVIEW: 'Manual review', FALLBACK_REVIEW: 'Manual review', }, diff --git a/client/src/i18n/locales/th-TH.ts b/client/src/i18n/locales/th-TH.ts index 8037d11..2f7802b 100644 --- a/client/src/i18n/locales/th-TH.ts +++ b/client/src/i18n/locales/th-TH.ts @@ -454,6 +454,43 @@ export default { taskQueue: 'คิวงานของออเดอร์นี้', v4TaskTimeline: 'ไทม์ไลน์งานออเดอร์ V4', v4TaskTimelineEmpty: 'ยังไม่มีงานออเดอร์ V4', + v4TaskTimelineHint: 'แสดงเฉพาะสรุปงานและการ์ดที่ปลอดภัย หากต้องดำเนินการให้เข้าไปที่หน้ารายละเอียดงาน V4', + v4OverviewTitle: 'สแนปช็อต V4 ที่ยืนยันแล้ว', + v4OverviewSubtitle: 'มาจากการ์ดที่ยืนยันแล้วเท่านั้น ไม่ใช้คำแนะนำ AI ที่ยังไม่ยืนยัน', + v4OverviewEmpty: 'ยังไม่มีสแนปช็อตออเดอร์ V4 ที่ยืนยันแล้ว', + accountCode: 'Account Code', + accountName: 'Account Name', + marketCode: 'Market Code', + sourceCode: 'Source Code', + departureDate: 'วันออก', + rateCode: 'Rate Code', + roomItems: 'ประเภทห้องและจำนวนที่ยืนยันแล้ว', + latestConfirmedAt: 'เวลายืนยันล่าสุด', + traceCardStatus: 'สถานะการ์ด Trace', + roomingListCardStatus: 'สถานะการ์ด Rooming List', + paymentCardStatus: 'สถานะการ์ด Payment', + nextV4ActionTitle: 'ขั้นตอนถัดไป', + nextV4ActionSubtitle: 'หน้าสรุปออเดอร์เป็นทางเข้าเท่านั้น ไม่ยืนยัน ตรวจสอบ หรือแก้ไขการ์ดในหน้านี้', + nextV4ActionType: 'ประเภทการดำเนินการ', + nextV4ActionStatus: 'สถานะการดำเนินการ', + nextV4CardId: 'รหัสการ์ด', + openOrderTaskCount: 'งาน V4 ที่ยังไม่เสร็จ', + enterV4Task: 'ไปหน้าดำเนินการ', + nextActionConfirm: 'ยืนยันการ์ด', + nextActionReview: 'ตรวจสอบด้วยคน', + nextActionNone: 'ไม่มีงานรอดำเนินการ', + nextV4ActionEmpty: 'ยังไม่มีขั้นตอน V4 ถัดไป', + relatedSourceMessages: 'ข้อความต้นทางที่เกี่ยวข้อง', + relatedSourceMessagesHint: 'แสดงเฉพาะหัวอีเมลและสรุปเธรด หากต้องการอ่านทั้งหมดให้เปิดหน้าบทสนทนาอีเมล', + relatedSourceMessagesEmpty: 'ยังไม่มีข้อความต้นทางที่เกี่ยวข้อง', + externalConversationId: 'รหัสเธรดภายนอก', + latestActivityAt: 'กิจกรรมล่าสุด', + v4CardsEmpty: 'ยังไม่มีสรุปการ์ด', + cardId: 'รหัสการ์ด', + reviewStatus: 'สถานะตรวจสอบ', + confirmedBy: 'ผู้ยืนยัน', + confirmedAt: 'เวลายืนยัน', + sourceEventIndex: 'เหตุการณ์ต้นทาง {index}', sourceMessage: 'ข้อความต้นทาง', businessKeySource: 'แหล่งที่มาของเลขธุรกิจ', updatedAt: 'อัปเดตล่าสุด', @@ -822,6 +859,12 @@ export default { INFORMATIONAL_MESSAGE: 'แจ้งเตือนข้อความ (เดิม)', SOURCE_MESSAGE_REVIEW_NOTIFICATION: 'แจ้งเตือนข้อความต้นทาง', SOURCE_MESSAGE_ONLY: 'ข้อความต้นทางแบบอ่านอย่างเดียว', + SOURCE_MESSAGE_DISPLAY: 'การ์ดแสดงข้อความต้นทาง', + SOURCE_MESSAGE_NOTIFICATION: 'การ์ดแจ้งเตือนข้อความต้นทาง', + BASIC_INFORMATION: 'การ์ดข้อมูลพื้นฐาน', + ROOM_INFORMATION: 'การ์ดข้อมูลห้อง', + TRACE_RESERVATION_NOTES: 'การ์ดหมายเหตุติดตาม', + PAYMENT: 'การ์ดการชำระเงิน', MANUAL_REVIEW: 'ตรวจสอบด้วยคน', FALLBACK_REVIEW: 'ตรวจสอบด้วยคน', }, diff --git a/client/src/i18n/locales/zh-CN.ts b/client/src/i18n/locales/zh-CN.ts index ba65784..82e15bf 100644 --- a/client/src/i18n/locales/zh-CN.ts +++ b/client/src/i18n/locales/zh-CN.ts @@ -454,6 +454,43 @@ export default { taskQueue: '同订单任务队列', v4TaskTimeline: 'V4 订单任务时间线', v4TaskTimelineEmpty: '暂无 V4 订单任务。', + v4TaskTimelineHint: '只展示订单任务和任务卡安全摘要;处理动作请进入 V4 任务详情页完成。', + v4OverviewTitle: 'V4 当前确认快照', + v4OverviewSubtitle: '仅来自已确认任务卡,不使用未确认 AI 建议。', + v4OverviewEmpty: '暂无已确认的 V4 订单快照。', + accountCode: 'Account 代码', + accountName: 'Account 名称', + marketCode: 'Market 代码', + sourceCode: 'Source 代码', + departureDate: '离店日期', + rateCode: 'Rate Code', + roomItems: '已确认房型房量', + latestConfirmedAt: '最近确认时间', + traceCardStatus: 'Trace 卡片状态', + roomingListCardStatus: 'Rooming List 卡片状态', + paymentCardStatus: 'Payment 卡片状态', + nextV4ActionTitle: '下一步处理', + nextV4ActionSubtitle: '订单总览只提供入口,不直接确认、复核或编辑任务卡。', + nextV4ActionType: '动作类型', + nextV4ActionStatus: '动作状态', + nextV4CardId: '任务卡编号', + openOrderTaskCount: '未完成 V4 任务数', + enterV4Task: '进入处理页', + nextActionConfirm: '确认任务卡', + nextActionReview: '人工复核', + nextActionNone: '无待处理动作', + nextV4ActionEmpty: '暂无下一步 V4 处理动作。', + relatedSourceMessages: '关联来源消息', + relatedSourceMessagesHint: '只展示邮件头和会话摘要;全文请进入邮件会话页查看。', + relatedSourceMessagesEmpty: '暂无关联来源消息。', + externalConversationId: '外部会话编号', + latestActivityAt: '最近活动时间', + v4CardsEmpty: '暂无任务卡摘要。', + cardId: '任务卡编号', + reviewStatus: '复核状态', + confirmedBy: '确认人', + confirmedAt: '确认时间', + sourceEventIndex: '来源事件 {index}', sourceMessage: '来源消息', businessKeySource: '业务号来源', updatedAt: '最近更新', @@ -822,6 +859,12 @@ export default { INFORMATIONAL_MESSAGE: '消息通知(历史 Message Notification)', SOURCE_MESSAGE_REVIEW_NOTIFICATION: '来源消息通知', SOURCE_MESSAGE_ONLY: '来源消息只读', + SOURCE_MESSAGE_DISPLAY: '来源消息展示卡', + SOURCE_MESSAGE_NOTIFICATION: '来源消息通知卡', + BASIC_INFORMATION: '基础信息卡', + ROOM_INFORMATION: '房型信息卡', + TRACE_RESERVATION_NOTES: '追踪备注卡', + PAYMENT: '付款信息卡', MANUAL_REVIEW: '人工复核', FALLBACK_REVIEW: '人工复核', }, diff --git a/client/src/tests/reservationViews.spec.ts b/client/src/tests/reservationViews.spec.ts index b66d3df..651ee8a 100644 --- a/client/src/tests/reservationViews.spec.ts +++ b/client/src/tests/reservationViews.spec.ts @@ -11,6 +11,7 @@ import ReservationSourceMessageConversationView from '@/views/reservation/Reserv import ReservationTaskListView from '@/views/reservation/ReservationTaskListView.vue' import { useAuthStore } from '@/stores/authStore' import type { + ReservationOrderDetailResult, ReservationOrderListResult, ReservationV4OrderTaskListResult, ReservationV4WorkbenchListResult, @@ -221,7 +222,7 @@ function createV4OrderTaskListResult( } } -function createOrderDetailResult(displayName = 'GRP-001') { +function createOrderDetailResult(displayName = 'GRP-001'): ReservationOrderDetailResult { return { order: { order_id: displayName === 'GRP-002' ? '20002' : '20001', @@ -286,6 +287,49 @@ function createOrderDetailResult(displayName = 'GRP-001') { review_required_count: 1, confirmed_count: 0, }, + cards: [ + { + card_id: '9100', + card_type: 'SOURCE_MESSAGE_DISPLAY', + event_type: null, + source_event_index: 0, + card_sort_order: 0, + card_status: 'READONLY', + review_status: null, + confirmed_by: null, + confirmed_at: null, + latest_activity_at: '2026-07-08T05:05:00Z', + ai_payload_json: 'unsafe ai payload must stay hidden', + display_payload_json: 'unsafe display payload must stay hidden', + confirmed_payload_json: 'unsafe confirmed payload must stay hidden', + text_body: 'unsafe mail body must stay hidden', + attachment_url: 'https://oss.example.test/private.xlsx', + }, + { + card_id: '9101', + card_type: 'BASIC_INFORMATION', + event_type: 'NEW_BOOKING', + source_event_index: 1, + card_sort_order: 10, + card_status: 'CONFIRMED', + review_status: null, + confirmed_by: 'frontend-query-admin', + confirmed_at: '2026-07-08T11:10:00Z', + latest_activity_at: '2026-07-08T11:10:00Z', + }, + { + card_id: '9104', + card_type: 'PAYMENT', + event_type: 'PAYMENT', + source_event_index: 3, + card_sort_order: 60, + card_status: 'REVIEW_REQUIRED', + review_status: 'PENDING', + confirmed_by: null, + confirmed_at: null, + latest_activity_at: '2026-07-08T11:20:00Z', + }, + ], source_message_summary: { source_message_id: '30003', hotel_id: 'HOTEL-TEST', @@ -303,8 +347,54 @@ function createOrderDetailResult(displayName = 'GRP-001') { latest_activity_at: '2026-07-08T05:10:00Z', }, ], + order_overview: { + account_code: 'QBD_TRAVEL', + account_name: 'Q.B.D. TRAVEL GROUP CO., LTD', + market_code: 'LEISURE', + source_code: 'TRAVEL_AGENT', + arrival_date: '2026-07-26', + departure_date: '2026-07-29', + rate_code: 'BAR', + room_items: [ + { + room_type_code: 'RM1', + room_count: 2, + }, + { + room_type_code: 'RM2', + room_count: 1, + }, + ], + trace_card_status: 'CONFIRMED', + rooming_list_card_status: 'PENDING_CONFIRM', + payment_card_status: 'REVIEW_REQUIRED', + latest_confirmed_at: '2026-07-08T11:20:00Z', + ai_payload_json: 'unsafe overview payload must stay hidden', + }, + next_v4_action: { + order_task_id: '9001', + card_id: '9104', + action_type: 'REVIEW', + action_status: 'REVIEW_REQUIRED', + open_order_task_count: 1, + }, + related_source_messages: [ + { + source_message_id: '30003', + hotel_id: 'HOTEL-TEST', + external_message_id: 'm-v4-1', + external_conversation_id: 'thread-v4-1', + subject: 'V4 Booking Package', + sender_summary: 'v4@example.test', + received_at: '2026-07-08T05:00:00Z', + source_sent_at: null, + conversation_message_count: 1, + html_body: 'unsafe related mail body', + attachment_url: 'https://oss.example.test/related.xlsx', + }, + ], warnings: [], - } + } as unknown as ReservationOrderDetailResult } function createSourceMessageConversationResult( @@ -480,28 +570,65 @@ describe('reservation P0 views', () => { vi.mocked(service.fetchSourceMessageConversation).mockReset() }) - it('renders active order task source evidence without the stale pending copy', async () => { + it('renders the V4 order overview, next action, related source messages and safe card timeline', async () => { vi.mocked(service.fetchReservationOrderDetail).mockResolvedValue(createOrderDetailResult()) - const wrapper = await mountWithPlugins(ReservationOrderDetailView, '/reservation/orders/20001', { - ReservationTaskDetailPanel: { - template: '
', - }, - }) + const wrapper = await mountWithPlugins(ReservationOrderDetailView, '/reservation/orders/20001') await vi.dynamicImportSettled() - expect(wrapper.text()).toContain('Booking Request') - expect(wrapper.text()).toContain('guest@example.test') - expect(wrapper.text()).toContain('2') - expect(wrapper.text()).toContain('前序任务未完成') + expect(wrapper.text()).toContain('V4 当前确认快照') + expect(wrapper.text()).toContain('QBD_TRAVEL') + expect(wrapper.text()).toContain('Q.B.D. TRAVEL GROUP CO., LTD') + expect(wrapper.text()).toContain('2026-07-26') + expect(wrapper.text()).toContain('2026-07-29') + expect(wrapper.text()).toContain('RM1 × 2') + expect(wrapper.text()).toContain('下一步处理') + expect(wrapper.find('a[href="/reservation/order-tasks/9001"]').text()).toContain('进入处理页') + expect(wrapper.find('.primary-link[href="/reservation/order-tasks/9001"]').exists()).toBe(true) + expect(wrapper.text()).toContain('关联来源消息') + expect(wrapper.text()).toContain('v4@example.test') expect(wrapper.text()).toContain('查看邮件会话') + expect(wrapper.find('a[href="/reservation/source-messages/30003/conversation"]').exists()).toBe(true) expect(wrapper.text()).toContain(zhCN.order.v4TaskTimeline) expect(wrapper.text()).toContain('order-ref-1') expect(wrapper.text()).toContain('V4 Booking Package') - expect(wrapper.text()).not.toContain('PREVIOUS_TASK_NOT_FINISHED') + expect(wrapper.find('.v4-timeline-item__main[href="/reservation/order-tasks/9001"]').exists()).toBe(true) + expect(wrapper.text()).toContain('基础信息卡') + expect(wrapper.text()).toContain('frontend-query-admin') + expect(wrapper.text()).toContain('待复核') + expect(wrapper.text()).not.toContain('unsafe ai payload must stay hidden') + expect(wrapper.text()).not.toContain('unsafe display payload must stay hidden') + expect(wrapper.text()).not.toContain('unsafe confirmed payload must stay hidden') + expect(wrapper.text()).not.toContain('unsafe mail body must stay hidden') + expect(wrapper.text()).not.toContain('https://oss.example.test') + expect(wrapper.findComponent({ name: 'ReservationTaskDetailPanel' }).exists()).toBe(false) expect(wrapper.text()).not.toContain('来源邮件摘要字段待后端补充') }) + it('shows empty states when an order has no V4 overview, next action, source messages or task timeline', async () => { + const result = createOrderDetailResult() + result.order_overview = null + result.next_v4_action = { + order_task_id: null, + card_id: null, + action_type: 'NONE', + action_status: null, + open_order_task_count: 0, + } + result.related_source_messages = [] + result.v4_order_tasks = [] + vi.mocked(service.fetchReservationOrderDetail).mockResolvedValue(result) + + const wrapper = await mountWithPlugins(ReservationOrderDetailView, '/reservation/orders/20001') + await vi.dynamicImportSettled() + + expect(wrapper.text()).toContain('暂无已确认的 V4 订单快照') + expect(wrapper.text()).toContain('暂无下一步 V4 处理动作') + expect(wrapper.text()).toContain('暂无关联来源消息') + expect(wrapper.text()).toContain(zhCN.order.v4TaskTimelineEmpty) + expect(wrapper.find('a[href^="/reservation/order-tasks/"]').exists()).toBe(false) + }) + it('renders order list items returned by the backend', async () => { vi.mocked(service.fetchReservationOrders).mockResolvedValue(createOrderListResult()) diff --git a/client/src/types/reservation.ts b/client/src/types/reservation.ts index 8c3fc7d..77b72d1 100644 --- a/client/src/types/reservation.ts +++ b/client/src/types/reservation.ts @@ -246,6 +246,36 @@ export interface ReservationV4CardCounts { confirmed_count: number } +export interface ReservationOrderV4RoomSummary { + room_type_code: string | null + room_count: number | null +} + +export interface ReservationOrderV4OverviewResult { + account_code: string | null + account_name: string | null + market_code: string | null + source_code: string | null + arrival_date: string | null + departure_date: string | null + rate_code: string | null + room_items: ReservationOrderV4RoomSummary[] + trace_card_status: ReservationV4CardStatus | string | null + rooming_list_card_status: ReservationV4CardStatus | string | null + payment_card_status: ReservationV4CardStatus | string | null + latest_confirmed_at: string | null +} + +export type ReservationOrderV4NextActionType = 'CONFIRM' | 'REVIEW' | 'NONE' | string + +export interface ReservationOrderV4NextActionResult { + order_task_id: string | null + card_id: string | null + action_type: ReservationOrderV4NextActionType | null + action_status: ReservationV4CardStatus | string | null + open_order_task_count: number | null +} + export interface ReservationV4ActionAvailability { blocked: boolean read_only: boolean @@ -453,6 +483,7 @@ export interface ReservationV4OrderTaskTimelineItem { order_ref: string | null order_task_status: ReservationV4OrderTaskStatus | string card_counts: ReservationV4CardCounts + cards: ReservationOrderV4TaskCardTimelineItem[] source_message_summary: ReservationV4SourceMessageSummary | null source_received_at: string | null created_at: string | null @@ -460,8 +491,24 @@ export interface ReservationV4OrderTaskTimelineItem { latest_activity_at: string | null } +export interface ReservationOrderV4TaskCardTimelineItem { + card_id: string + card_type: ReservationV4CardType | string + event_type: string | null + source_event_index: number | null + card_sort_order: number | null + card_status: ReservationV4CardStatus | string + review_status: string | null + confirmed_by: string | null + confirmed_at: string | null + latest_activity_at: string | null +} + export interface ReservationOrderDetailResult { order: ReservationOrderSummaryResult + order_overview: ReservationOrderV4OverviewResult | null + next_v4_action: ReservationOrderV4NextActionResult | null + related_source_messages: ReservationV4SourceMessageSummary[] tasks: ReservationOrderTaskTimelineItem[] v4_order_tasks: ReservationV4OrderTaskTimelineItem[] warnings: string[] diff --git a/client/src/views/reservation/ReservationOrderDetailView.vue b/client/src/views/reservation/ReservationOrderDetailView.vue index f440ec9..77d27d6 100644 --- a/client/src/views/reservation/ReservationOrderDetailView.vue +++ b/client/src/views/reservation/ReservationOrderDetailView.vue @@ -51,110 +51,290 @@
-
- +
{{ fact.label }}
+
{{ fact.value }}
+
+ - -
- {{ t('common.noData') }} +
+ {{ t('order.roomItems') }} +
+ + {{ formatRoomSummary(room) }} + +
+ - +
+ +
+
+ {{ statusItem.label }} + + - +
+
+ +

+ {{ t('order.v4OverviewEmpty') }} +

+
+ +
+
+
+

+ {{ t('order.nextV4ActionTitle') }} +

+

+ {{ t('order.nextV4ActionSubtitle') }} +

+
+
+ + +

+ {{ t('order.nextV4ActionEmpty') }} +

+ +
+
+
+

+ {{ t('order.relatedSourceMessages') }} +

+

+ {{ t('order.relatedSourceMessagesHint') }} +

+
+
+ +
+
+
+ {{ sourceMessage.subject ?? t('taskList.noSourceSubject') }} + {{ sourceMessage.sender_summary ?? '-' }} +
+
+
+
{{ t('task.sourceReceivedAt') }}
+
{{ formatReservationDateTime(sourceMessage.received_at) }}
+
+
+
{{ t('order.conversationMessageCount') }}
+
{{ sourceMessage.conversation_message_count ?? '-' }}
+
+
+
{{ t('order.externalConversationId') }}
+
{{ sourceMessage.external_conversation_id ?? '-' }}
+
+
+ + {{ t('task.viewConversation') }} + +
+
+

+ {{ t('order.relatedSourceMessagesEmpty') }} +

+
+ +
+
+
+

+ {{ t('order.v4TaskTimeline') }} +

+

+ {{ t('order.v4TaskTimelineHint') }} +

+
+
+ +

+ {{ t('order.v4TaskTimelineEmpty') }} +

+
+
+ + + {{ v4OrderReference(item) }} + {{ item.source_message_summary?.subject ?? t('taskList.noSourceSubject') }} + + + + +
+
+
{{ t('taskList.cardCountsTotal', { total: item.card_counts.total_count }) }}
+
+ {{ t('taskList.cardCountsPending', { count: item.card_counts.pending_confirm_count }) }} + · + {{ t('taskList.cardCountsReview', { count: item.card_counts.review_required_count }) }} + · + {{ t('taskList.cardCountsConfirmed', { count: item.card_counts.confirmed_count }) }} +
+
+
+
{{ t('task.sourceReceivedAt') }}
+
{{ formatReservationDateTime(item.source_received_at) }}
+
+
+
{{ t('order.latestActivityAt') }}
+
{{ formatReservationDateTime(item.latest_activity_at ?? item.updated_at ?? item.created_at) }}
+
+
+ +
+
+
+ + {{ cardTimelineTitle(card) }} + {{ card.event_type ?? '-' }} + + +
+
+
+
{{ t('order.cardId') }}
+
{{ card.card_id }}
+
+
+
{{ t('order.reviewStatus') }}
+
+ + - +
+
+
+
{{ t('order.confirmedBy') }}
+
{{ card.confirmed_by ?? '-' }}
+
+
+
{{ t('order.confirmedAt') }}
+
{{ formatReservationDateTime(card.confirmed_at) }}
+
+
+
{{ t('order.latestActivityAt') }}
+
{{ formatReservationDateTime(card.latest_activity_at) }}
+
+
+
+
+

+ {{ t('order.v4CardsEmpty') }} +

+
+
+
@@ -165,11 +345,16 @@ import { RouterLink, useRoute } from 'vue-router' import { useI18n } from 'vue-i18n' import ReservationStatusBadge from '@/components/reservation/ReservationStatusBadge.vue' -import ReservationTaskDetailPanel from '@/components/reservation/ReservationTaskDetailPanel.vue' -import ReservationTaskQueue from '@/components/reservation/ReservationTaskQueue.vue' import { fetchReservationOrderDetail } from '@/services/reservationService' import { useAuthStore } from '@/stores/authStore' -import type { ReservationOrderDetailResult, ReservationV4OrderTaskTimelineItem } from '@/types/reservation' +import type { + ReservationOrderDetailResult, + ReservationOrderV4NextActionResult, + ReservationOrderV4RoomSummary, + ReservationOrderV4TaskCardTimelineItem, + ReservationV4OrderTaskTimelineItem, +} from '@/types/reservation' +import { formatReservationTaskCard } from '@/utils/reservationDisplay' import { formatReservationDateTime } from '@/utils/reservationFormat' const route = useRoute() @@ -178,11 +363,61 @@ const authStore = useAuthStore() const loading = ref(false) const errorMessage = ref('') const detail = ref(null) -const activeTaskId = ref('') const orderId = computed(() => String(route.params.orderId ?? '')) -const activeTask = computed(() => detail.value?.tasks.find((task) => task.task_id === activeTaskId.value) ?? null) +const orderOverview = computed(() => detail.value?.order_overview ?? null) +const nextV4Action = computed(() => detail.value?.next_v4_action ?? null) +const relatedSourceMessages = computed(() => detail.value?.related_source_messages ?? []) const v4OrderTasks = computed(() => detail.value?.v4_order_tasks ?? []) +const overviewRoomItems = computed(() => orderOverview.value?.room_items ?? []) +const hasNextV4Action = computed(() => { + const action = nextV4Action.value + return Boolean(action?.order_task_id && action.action_type && action.action_type !== 'NONE') +}) +const hasOrderOverview = computed(() => { + const overview = orderOverview.value + if (!overview) { + return false + } + return Boolean( + overview.account_code || + overview.account_name || + overview.market_code || + overview.source_code || + overview.arrival_date || + overview.departure_date || + overview.rate_code || + overview.room_items.length || + overview.trace_card_status || + overview.rooming_list_card_status || + overview.payment_card_status || + overview.latest_confirmed_at, + ) +}) +const overviewFacts = computed(() => { + const overview = orderOverview.value + if (!overview) { + return [] + } + return [ + { label: t('order.accountCode'), value: displayValue(overview.account_code) }, + { label: t('order.accountName'), value: displayValue(overview.account_name) }, + { label: t('order.marketCode'), value: displayValue(overview.market_code) }, + { label: t('order.sourceCode'), value: displayValue(overview.source_code) }, + { label: t('order.arrivalDate'), value: displayValue(overview.arrival_date) }, + { label: t('order.departureDate'), value: displayValue(overview.departure_date) }, + { label: t('order.rateCode'), value: displayValue(overview.rate_code) }, + { label: t('order.latestConfirmedAt'), value: formatReservationDateTime(overview.latest_confirmed_at) }, + ] +}) +const overviewStatuses = computed(() => { + const overview = orderOverview.value + return [ + { label: t('order.traceCardStatus'), status: overview?.trace_card_status ?? null }, + { label: t('order.roomingListCardStatus'), status: overview?.rooming_list_card_status ?? null }, + { label: t('order.paymentCardStatus'), status: overview?.payment_card_status ?? null }, + ] +}) watch( orderId, @@ -206,21 +441,45 @@ async function loadOrder(nextOrderId: string): Promise { loading.value = true errorMessage.value = '' try { - const nextOrder = await fetchReservationOrderDetail(nextOrderId) - detail.value = nextOrder - activeTaskId.value = nextOrder.tasks[0]?.task_id ?? '' + detail.value = await fetchReservationOrderDetail(nextOrderId) } catch (error) { detail.value = null - activeTaskId.value = '' errorMessage.value = error instanceof Error ? error.message : String(error) } finally { loading.value = false } } +function displayValue(value: string | null | undefined): string { + return value?.trim() || '-' +} + +function formatRoomSummary(room: ReservationOrderV4RoomSummary): string { + return `${displayValue(room.room_type_code)} × ${room.room_count ?? '-'}` +} + +function formatNextActionType(actionType: ReservationOrderV4NextActionResult['action_type']): string { + if (actionType === 'CONFIRM') { + return t('order.nextActionConfirm') + } + if (actionType === 'REVIEW') { + return t('order.nextActionReview') + } + if (actionType === 'NONE') { + return t('order.nextActionNone') + } + return actionType ?? '-' +} + function v4OrderReference(item: ReservationV4OrderTaskTimelineItem): string { return item.order_ref?.trim() || item.source_message_summary?.subject?.trim() || `#${item.order_task_id}` } + +function cardTimelineTitle(card: ReservationOrderV4TaskCardTimelineItem): string { + const title = formatReservationTaskCard(t, card.card_type, card.card_type) + const index = card.source_event_index == null ? '' : ` · ${t('order.sourceEventIndex', { index: card.source_event_index })}` + return `${title}${index}` +} diff --git a/docs/project/frontend-backend/backend-to-frontend-notes.md b/docs/project/frontend-backend/backend-to-frontend-notes.md index 91cfca7..964aa92 100644 --- a/docs/project/frontend-backend/backend-to-frontend-notes.md +++ b/docs/project/frontend-backend/backend-to-frontend-notes.md @@ -106,7 +106,7 @@ | --- | --- | --- | | `GET /api/reservation/orders` | 补齐订单列表 V4 继续处理入口字段,并新增统一 open count 字段;前端展示已接入。 | `order_status` 不传时默认查询全部订单状态;`page_num` 从 1 开始;`page_size` 后端有最大值保护;`open_work_item_count` 是订单列表展示用统一待处理数量,开发阶段不考虑旧数据,第一版直接等于 `v4_open_order_task_count`;前端展示待处理数量时只读该字段,不自行计算旧任务数和 V4 数,也不使用旧 `open_task_count` 作为展示数量;旧 `open_task_count` 和 `next_processable_task_id` 继续保留用于 V2/V3 兼容与排查。V4 新增 `next_v4_order_task_id`、`next_v4_action_card_id`、`next_v4_action_type`、`next_v4_action_status`、`v4_open_order_task_count`;前端“继续处理”已按优先级实现:存在 `next_v4_order_task_id` 时跳 `/reservation/order-tasks/{next_v4_order_task_id}`,否则回退旧 `/reservation/tasks/{next_processable_task_id}`;两者都没有时展示无待处理状态。 | | `GET /api/reservation/tasks` | 补齐来源邮件会话摘要字段,并新增 `order_status` 查询参数。 | `order_status` 按任务所属订单状态过滤,支持 `TEMPORARY`、`ACTIVE`、`ENDED`、`LOGIC_DELETED`;列表仍然只返回安全摘要,不返回正文、HTML、附件 URL 或 AI 原始 payload;点击邮件入口时使用 `source_message_id` 调会话详情。 | -| `GET /api/reservation/orders/{orderId}` | 补齐旧 `tasks[]` 来源邮件会话摘要字段,并新增 V4 总览和 `v4_order_tasks[]` 订单任务时间线。 | `include_tasks=false` 可只取轻量摘要,此时 `tasks[]`、`v4_order_tasks[]` 和 `related_source_messages[]` 都为空,`order_overview` 为空快照,`next_v4_action.action_type=NONE`;旧 `tasks[]` 顺序由后端按订单队列返回,V4 `v4_order_tasks[]` 按 `source_received_at`、`source_message_id`、`order_context_index`、`created_at`、数字 ID 正序返回;前端不要自行重排。`include_source_summary` 第一版不作为前端裁剪字段的强约束,前端暂不要依赖它减少返回字段。 | +| `GET /api/reservation/orders/{orderId}` | 补齐旧 `tasks[]` 来源邮件会话摘要字段,并新增 V4 总览和 `v4_order_tasks[]` 订单任务时间线;前端订单详情总览页已接入。 | `include_tasks=false` 可只取轻量摘要,此时 `tasks[]`、`v4_order_tasks[]` 和 `related_source_messages[]` 都为空,`order_overview` 为空快照,`next_v4_action.action_type=NONE`;旧 `tasks[]` 顺序由后端按订单队列返回,V4 `v4_order_tasks[]` 按 `source_received_at`、`source_message_id`、`order_context_index`、`created_at`、数字 ID 正序返回;前端不要自行重排。订单详情页只展示 V4 确认快照、下一步入口、关联来源消息和任务卡安全摘要,不在该页确认、复核或编辑任务卡,也不展示 payload、邮件正文或附件 URL。`include_source_summary` 第一版不作为前端裁剪字段的强约束,前端暂不要依赖它减少返回字段。 | | `GET /api/reservation/tasks/{taskId}` | 补齐顶层来源邮件字段,并扩展 `fields[]` 元数据。 | 顶层来源字段用于打开邮件会话;`fields[]` 中的 `result_type`、`task_type`、`task_subtype`、`default_value_source` 用于前端字段分组、调试和白名单对齐。 | | `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 解析失败。 | @@ -157,7 +157,7 @@ POST /api/auth/logout | `external_conversation_id` | 外部邮件会话 ID。 | 仅用于展示或调试,不作为当前会话详情接口路径参数。 | | `conversation_message_count` | 同一外部会话下的邮件数量。 | 用于提示用户打开的是整段会话,不是单封邮件。 | -订单详情已经补齐 V4 订单页总览字段:`order_overview`、`next_v4_action`、`related_source_messages[]` 和增强后的 `v4_order_tasks[].cards[]`。订单详情页应定位为订单视角总览,不要在订单详情页直接编辑、确认或复核任务卡;点击 `next_v4_action.order_task_id` 或时间线 `order_task_id` 后进入 `/reservation/order-tasks/{orderTaskId}` 对应的 V4 订单任务详情页处理。 +订单详情已经补齐并完成前端接入 V4 订单页总览字段:`order_overview`、`next_v4_action`、`related_source_messages[]` 和增强后的 `v4_order_tasks[].cards[]`。订单详情页定位为订单视角总览,不要在订单详情页直接编辑、确认或复核任务卡;点击 `next_v4_action.order_task_id` 或时间线 `order_task_id` 后进入 `/reservation/order-tasks/{orderTaskId}` 对应的 V4 订单任务详情页处理。 `order_overview` 只从已确认 V4 卡片派生:Basic Information 未确认时 Account / Market / Source 为空;Room Information 未确认时日期、Rate Code、房型房量为空。前端不要把未确认卡片 display payload 反推成订单事实。 @@ -165,7 +165,7 @@ POST /api/auth/logout | 字段 | 说明 | 前端使用方式 | | --- | --- | --- | -| `order_task_id` | V4 订单任务 ID,字符串。 | 点击 V4 时间线项时跳转 `/api/reservation/order-tasks/{orderTaskId}` 对应的前端详情页。 | +| `order_task_id` | V4 订单任务 ID,字符串。 | 点击 V4 时间线项时跳转前端路由 `/reservation/order-tasks/{orderTaskId}`,该页面再调用 `GET /api/reservation/order-tasks/{orderTaskId}`。 | | `order_ref` | SuperAgent V4 回调包内订单引用。 | 用于区分同一邮件里的多个订单上下文,不等同 PMS 永久订单号。 | | `order_task_status` | V4 订单任务状态,当前为 `OPEN` / `COMPLETED`。 | 展示处理状态,不要用于替代卡片级 `availability`。 | | `card_counts` | V4 卡片数量摘要。 | 展示待确认、待复核和已确认规模。 | @@ -504,6 +504,7 @@ RESERVATION_ROOMING_LIST_GENERATE - 前端不展示 `ai_payload_json`、邮件完整正文、附件 URL、raw evidence 或 SuperAgent 原始 payload;来源邮件详情仍从邮件会话页面查看,卡片内仅展示后端普通接口返回的安全摘要、邮件片段和附件名称。 - V4 来源消息卡读取 `attachments`、`uploaded_media`、`file_references` 时,只允许展示附件名称、类型和大小等安全摘要;如果后端 payload 中异常出现 `https://`、`oss://`、`s3://` 等直接 URL 字符串,前端必须替换为“未命名附件”或隐藏,不得把 URL 渲染到普通业务页面。 - V4 主流程不调用旧 V2/V3 草稿、旧任务确认、旧同卡复核接口,也不展示 OPERA 模拟操作入口。 +- 2026-07-20 测试机 smoke 注意:订单详情页依赖 `GET /api/reservation/orders/{orderId}` 返回 `order_overview`、`next_v4_action`、`related_source_messages[]` 和 `v4_order_tasks[].cards[]`。如果测试机响应仍只有旧 `order`、`tasks[]`、基础 `v4_order_tasks[]` 和 `warnings`,应先确认测试机后端是否部署了包含 M002 V4 CP15.1 的最新包;前端不要为了该旧响应重新做兼容逻辑,避免把部署问题固化成页面分支。 ## 6. 不给前端直接调用的接口 diff --git a/docs/project/frontend-backend/frontend-to-backend-api-requests.md b/docs/project/frontend-backend/frontend-to-backend-api-requests.md index 32c6519..5a7286c 100644 --- a/docs/project/frontend-backend/frontend-to-backend-api-requests.md +++ b/docs/project/frontend-backend/frontend-to-backend-api-requests.md @@ -10,7 +10,7 @@ | --- | --- | --- | --- | | P0 | 订单列表接口 `GET /api/reservation/orders` | 订单列表页、首页工作台 | 已完成第一版 | | P0 | 任务列表接口 `GET /api/reservation/tasks` | 任务列表菜单、订单详情任务入口 | 已完成第一版,已补来源邮件会话字段和 `order_status` 筛选 | -| P0 | 订单详情接口 `GET /api/reservation/orders/{orderId}` | 订单详情页 | 已完成第一版,已补旧 `tasks[]` 来源邮件会话字段和 V4 `v4_order_tasks[]` 时间线 | +| P0 | 订单详情接口 `GET /api/reservation/orders/{orderId}` | 订单详情页 | 已完成第一版,已补旧 `tasks[]` 来源邮件会话字段和 V4 `order_overview` / `next_v4_action` / `related_source_messages[]` / `v4_order_tasks[].cards[]`,前端订单详情总览页已接入 | | P0 | 任务详情读取接口 `GET /api/reservation/tasks/{taskId}` | 任务详情页动态渲染 | 已完成第一版,已补来源邮件字段和 3.0 字段元数据 | | P0 | 任务详情操作接口 | 任务详情保存、确认、OPERA、审计 | 已完成;前端可直接接入 | | P0 | 邮件会话详情接口 `GET /api/source-messages/{sourceMessageId}/conversation` | 邮件会话详情页 | 已完成第一版 | @@ -30,7 +30,7 @@ | 接口 / 能力 | 当前后端状态 | 前端是否可直接接入 | 仍需后端处理 | | --- | --- | --- | --- | | `GET /api/reservation/tasks` | 已完成第一版,已补来源邮件会话字段和所属订单状态筛选 | 可以 | `order_status` 按任务所属订单状态过滤;不传时保持当前全部任务列表行为。 | -| `GET /api/reservation/orders/{orderId}` | 已完成第一版,已补旧 `tasks[]` 来源邮件会话字段、V4 总览和 V4 `v4_order_tasks[]` 时间线 | 可以 | 暂无;`include_tasks=false` 时旧 `tasks[]`、V4 `v4_order_tasks[]` 和 `related_source_messages[]` 都返回空数组,`order_overview` 为空快照,`next_v4_action.action_type=NONE`。 | +| `GET /api/reservation/orders/{orderId}` | 已完成第一版,已补旧 `tasks[]` 来源邮件会话字段、V4 总览和 V4 `v4_order_tasks[]` 时间线,前端订单详情总览页已接入 | 可以 | 暂无;`include_tasks=false` 时旧 `tasks[]`、V4 `v4_order_tasks[]` 和 `related_source_messages[]` 都返回空数组,`order_overview` 为空快照,`next_v4_action.action_type=NONE`。 | | `GET /api/reservation/tasks/{taskId}` | 已完成第一版,已补任务顶层来源邮件字段和 `fields[]` 3.0 元数据 | 可以 | 当前 Controller 不接收 `hotel_id`;如后续多酒店隔离需要前端显式传酒店上下文,请后端补可选入参或确认按 taskId 全局唯一即可。 | | `PUT /api/reservation/tasks/{taskId}/draft` | 已完成 | 可以 | 当前 Controller 不接收 `hotel_id`;如写操作需要酒店上下文幂等 / 权限校验,请后端补可选入参或请求体字段。 | | `POST /api/reservation/tasks/{taskId}/confirm` | 已完成 | 可以 | 当前 Controller 不接收 `hotel_id`;如写操作需要酒店上下文幂等 / 权限校验,请后端补可选入参或请求体字段。 | @@ -165,9 +165,9 @@ GET /api/reservation/tasks GET /api/reservation/orders/{orderId} ``` -当前状态:后端已按 P0 最小诉求实现第一版,并在 M002 V4 CP15.1 补齐订单详情 V4 总览字段。接口返回订单摘要、V4 当前确认快照、V4 下一步处理入口、关联来源邮件摘要、旧 V2/V3 同订单任务时间线和 V4 订单任务时间线;`include_tasks=false` 时旧 `tasks[]`、V4 `v4_order_tasks[]` 和 `related_source_messages[]` 都返回空数组,`order_overview` 为空快照,`next_v4_action.action_type=NONE`。旧任务时间线已补齐每个任务的来源邮件会话摘要。 +当前状态:后端已按 P0 最小诉求实现第一版,并在 M002 V4 CP15.1 补齐订单详情 V4 总览字段;前端订单详情总览页已接入这些字段。接口返回订单摘要、V4 当前确认快照、V4 下一步处理入口、关联来源邮件摘要、旧 V2/V3 同订单任务时间线和 V4 订单任务时间线;`include_tasks=false` 时旧 `tasks[]`、V4 `v4_order_tasks[]` 和 `related_source_messages[]` 都返回空数组,`order_overview` 为空快照,`next_v4_action.action_type=NONE`。旧任务时间线已补齐每个任务的来源邮件会话摘要。 -订单详情页后续应定位为“订单总览 + 当前确认快照 + V4 任务时间线 + 下一步入口”,不是 V4 任务卡处理页。同订单任务队列里的每个任务都需要自己的“查看邮件会话”入口,因为不同任务可能来自不同邮件或不同邮件会话。因此 `tasks[]` 中每个任务已补齐来源邮件会话字段。V4 新模型下,同一订单的多卡订单任务通过 `v4_order_tasks[]` 单独返回,前端点击后进入 V4 订单任务详情。 +订单详情页当前定位为“订单总览 + 当前确认快照 + V4 任务时间线 + 下一步入口”,不是 V4 任务卡处理页。同订单任务队列里的每个任务都需要自己的“查看邮件会话”入口,因为不同任务可能来自不同邮件或不同邮件会话。因此 `tasks[]` 中每个任务已补齐来源邮件会话字段。V4 新模型下,同一订单的多卡订单任务通过 `v4_order_tasks[]` 单独返回,前端点击后进入 V4 订单任务详情。 建议入参: diff --git a/docs/project/requirements/M002-v4-test-machine-smoke-checklist.md b/docs/project/requirements/M002-v4-test-machine-smoke-checklist.md index 92c4b60..50922e1 100644 --- a/docs/project/requirements/M002-v4-test-machine-smoke-checklist.md +++ b/docs/project/requirements/M002-v4-test-machine-smoke-checklist.md @@ -8,7 +8,7 @@ | 日期 | 2026-07-19 | | 状态 | 测试机 V4 前后端联调冒烟清单 | | 适用范围 | 登录、酒店权限、V4 工作台、V4 订单任务详情、目录 lookup、卡片确认、复核解阻、S10/S99 ack、订单详情 V4 时间线 | -| 不适用范围 | 真实 OPERA / OHIP、目录同步、旧 V2/V3 全量回归、前端视觉验收 | +| 不适用范围 | 真实 OPERA / OHIP、目录同步、旧 V2/V3 全量回归、M011 CP4、前端视觉验收 | ## 1. 测试前置 @@ -256,15 +256,19 @@ ORDER BY updated_at DESC, id DESC; | 项目 | 结果 | 备注 | | --- | --- | --- | -| 登录与酒店权限 | 待测 | | -| V4 工作台 | 待测 | | -| V4 订单任务详情 | 待测 | | -| Account Lookup | 待测 | | -| Room Type Lookup | 待测 | | -| Rate Code Lookup | 待测 | | -| Basic Information 确认 | 待测 | | -| 业务卡确认 | 待测 | | -| REVIEW_REQUIRED 复核解阻 | 待测 | | -| S10/S99 ack | 待测 | | -| 订单详情 V4 时间线 | 待测 | | -| 目录管理 CP1 | 待测 | | +| 登录与酒店权限 | 通过 | 2026-07-20 smoke:`/api/auth/me` 返回 `HOTEL-TEST` 和 V4 权限;未带 Bearer 访问 V4 工作台返回 401;错误 `hotel_id` 访问工作台和订单详情返回 403。 | +| V4 工作台 | 通过 | `GET /api/reservation/workbench-items` 返回 5 条,包含 `ORDER_TASK` 和 `SOURCE_NOTIFICATION`;列表未发现邮件正文、附件 URL、AI 原始 payload 字段。 | +| V4 订单任务列表 | 通过 | `GET /api/reservation/order-tasks` 返回业务订单任务,不包含来源通知;`card_status=REVIEW_REQUIRED` 查询当前测试数据返回 0 条。 | +| V4 订单任务详情 | 通过 | `GET /api/reservation/order-tasks/2079096313548677122` 返回 `source_message_card`、`basic_information_card`、`business_cards[]`、`fields[]` 和 `availability`;Basic 未确认前业务卡 `readonly_reason_code=PRIOR_CARD_NOT_CONFIRMED`。 | +| Account Lookup | 通过 | `GET /api/reservation/lookups/accounts` 返回 3 条 ACTIVE Account。 | +| Room Type Lookup | 通过 | `GET /api/reservation/lookups/room-types` 返回 8 条 ACTIVE Room Type。 | +| Rate Code Lookup | 通过 | `GET /api/reservation/lookups/rate-codes` 返回 5 条 ACTIVE Rate Code,`stale=false`;有固定种子 warning,符合当前第一版目录状态。 | +| Basic Information 确认 | 通过 | 使用 `version=0` 确认 Basic Information 成功,卡片进入 `CONFIRMED`,version 递增到 1,业务卡随后变为可确认。 | +| 业务卡确认 | 通过 | 使用 `version=0` 确认 Room Information 成功,订单任务进入 `COMPLETED`,工作台显示 `display_status=COMPLETED`,订单列表 `open_work_item_count=0`、`v4_open_order_task_count=0`。 | +| REVIEW_REQUIRED 复核解阻 | 未覆盖写操作 | 当前测试机没有 `REVIEW_REQUIRED` 待复核卡;已用历史完成记录 `2078906313578168322` 验证详情和 `V4_CARD_REVIEW_RESOLVE` 审计查询返回脱敏结果。下一轮需要准备一条待复核测试数据后再跑写操作。 | +| S10/S99 来源通知详情 | 通过 | 工作台包含 S10 与 S99 来源通知;详情接口返回通知摘要和来源消息卡,不返回订单任务或业务卡。 | +| S10/S99 ack | 通过 | 对已 ACK 的 S10 通知重复 ack 返回 200 且保持 `ACKED`,验证幂等;S10/S99 均未创建订单,也未进入订单列表。 | +| V4 业务审计 | 通过 | 订单任务确认审计和来源通知 ack 审计接口返回 200;快照未发现 `ai_payload_json`、正文、HTML、附件 URL、token、secret 等敏感字段。 | +| 订单详情 V4 总览 / 时间线 | 失败 | 测试机 `GET /api/reservation/orders/2079096313393487874` 返回 200,但响应仍只有 `order`、旧 `tasks[]`、基础 `v4_order_tasks[]` 和 `warnings`;缺少 CP15.1 要求的 `order_overview`、`next_v4_action`、`related_source_messages[]` 和 `v4_order_tasks[].cards[]`。本地最新后端已有测试覆盖,优先排查测试机是否部署了包含 CP15.1 的最新包。 | +| M011 AgentBus Booking Excel 预处理观察 | 未覆盖 outbound payload | `GET /api/system/agentbus-probe` 显示测试机 AgentBus 已连接且已捕获 frame;当前普通 HTTP 接口不暴露发给 SuperAgent 的 outbound payload,无法仅通过本次 smoke 确认 `attachment_extractions[]` 是否追加。未推进 M011 CP4,未新增解析批次表或行级持久化。 | +| 目录管理 CP1 | 未覆盖 | 本 checkpoint 聚焦 V4 主流程联调;目录管理后台另行 smoke。 |