diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index c616d1f..dd51444 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -5,14 +5,14 @@ | 最近更新 | 2026-07-20 | | 当前分支 | `feature/huangting` | | 当前阶段 | M002 V4 入站、多卡模型、持久化基线、入站写入、查询接口、卡片确认、复核解阻、目录校验、订单详情时间线、DB 目录、Lookup API、前端 lookup 接入、目录管理后台 CP1 前后端和订单列表 V4 继续处理入口 | -| 当前重点 | M002 V4 已完成订单列表 V4 继续处理入口字段,`GET /api/reservation/orders` 可返回 V4 下一步订单任务、卡片、动作类型、动作状态和 open 数;后续可做测试机联调、真实 PMS / OPERA / OHIP 同步或 SuperAgent 目录供给方案 | +| 当前重点 | M002 V4 已完成订单列表 V4 继续处理入口字段和前端消费,`GET /api/reservation/orders` 可返回 V4 下一步订单任务、卡片、动作类型、动作状态和 open 数;前端“继续处理”已按 V4 优先、旧任务回退跳转;后续可做测试机联调、真实 PMS / OPERA / OHIP 同步或 SuperAgent 目录供给方案 | ## 1. 当前 Checkpoint - 名称:`M002-V4-CP14-order-list-v4-entrypoint` -- 状态:Done,订单列表接口已补齐 V4 下一步处理入口字段:`next_v4_order_task_id`、`next_v4_action_card_id`、`next_v4_action_type`、`next_v4_action_status`、`v4_open_order_task_count`。旧 `open_task_count` 和 `next_processable_task_id` 保持兼容。 +- 状态:Done,订单列表接口已补齐 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` 跳转 V4 订单任务详情,并在无 V4 入口时回退旧 `next_processable_task_id`。 - 目标:让前端订单列表“继续处理”在存在 V4 待处理订单任务时可优先跳转 V4 订单任务详情;无 V4 待处理时继续回退旧 V2/V3 任务入口。 -- 边界:本轮不改前端页面、不改 V4 确认 / 复核接口、不接真实 OPERA / OHIP、不废弃旧 V2/V3 任务入口、不把 S10/S99 来源通知放进订单列表。 +- 边界:本轮不改后端接口、不改 V4 确认 / 复核接口、不接真实 OPERA / OHIP、不废弃旧 V2/V3 任务入口、不把 S10/S99 来源通知放进订单列表。 ## 2. 当前优先级 @@ -35,7 +35,7 @@ - 后续每完成一个 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 下一步订单任务、卡片、动作类型、动作状态和 open 数。真实 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 下一步订单任务、卡片、动作类型、动作状态和 open 数,前端按 V4 优先、旧任务回退跳转。真实 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 diff --git a/client/src/tests/reservationService.spec.ts b/client/src/tests/reservationService.spec.ts index 204a773..346f09e 100644 --- a/client/src/tests/reservationService.spec.ts +++ b/client/src/tests/reservationService.spec.ts @@ -391,6 +391,11 @@ describe('reservationService real API mode', () => { display_name: 'GRP-001', open_task_count: 2, next_processable_task_id: '10002', + next_v4_order_task_id: '9001', + next_v4_action_card_id: '9101', + next_v4_action_type: 'REVIEW', + next_v4_action_status: 'REVIEW_REQUIRED', + v4_open_order_task_count: 1, created_at: '2026-07-08T03:00:00Z', updated_at: '2026-07-08T03:10:00Z', }, @@ -415,6 +420,10 @@ describe('reservationService real API mode', () => { ) expect(result.items[0]?.display_order_key).toBe('GRP-001') expect(result.items[0]?.next_processable_task_id).toBe('10002') + expect(result.items[0]?.next_v4_order_task_id).toBe('9001') + expect(result.items[0]?.next_v4_action_type).toBe('REVIEW') + expect(result.items[0]?.next_v4_action_status).toBe('REVIEW_REQUIRED') + expect(result.items[0]?.v4_open_order_task_count).toBe(1) }) it('passes order status and business-key filters to the backend', async () => { diff --git a/client/src/tests/reservationViews.spec.ts b/client/src/tests/reservationViews.spec.ts index b2ba4e7..3055a9e 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 { + ReservationOrderListResult, ReservationV4OrderTaskListResult, ReservationV4WorkbenchListResult, SourceMessageConversationResult, @@ -38,7 +39,8 @@ function createOrderListResult( page_size: 20, total: 1, }, -) { + orderOverrides: Partial = {}, +): ReservationOrderListResult { return { items: [ { @@ -54,6 +56,7 @@ function createOrderListResult( next_processable_task_id: '10002', created_at: '2026-07-08T03:00:00Z', updated_at: '2026-07-08T03:10:00Z', + ...orderOverrides, }, ], page, @@ -455,7 +458,8 @@ async function mountWithPlugins(component: object, initialPath = '/', stubs: Rec plugins: [pinia, i18n, router], stubs: { RouterLink: { - template: '', + props: ['to'], + template: '', }, ...stubs, }, @@ -512,6 +516,83 @@ describe('reservation P0 views', () => { expect(wrapper.text()).not.toContain('接口待接入') }) + it('routes continue action to the V4 order task when the backend returns next_v4_order_task_id', async () => { + vi.mocked(service.fetchReservationOrders).mockResolvedValue(createOrderListResult('GRP-001', { + page_num: 1, + page_size: 20, + total: 1, + }, { + next_v4_order_task_id: '9001', + next_v4_action_card_id: '9101', + next_v4_action_type: 'CONFIRM', + next_v4_action_status: 'PENDING_CONFIRM', + v4_open_order_task_count: 1, + })) + + const wrapper = await mountWithPlugins(ReservationOrderListView) + await vi.dynamicImportSettled() + + expect(wrapper.find('a[href="/reservation/orders/20001"]').text()).toContain('查看详情') + expect(wrapper.find('a[href="/reservation/order-tasks/9001"]').text()).toContain('继续处理') + expect(wrapper.find('a[href="/reservation/tasks/10002"]').exists()).toBe(false) + expect(wrapper.text()).toContain('待确认') + }) + + it('shows review state for V4 review entrypoints in the order list', async () => { + vi.mocked(service.fetchReservationOrders).mockResolvedValue(createOrderListResult('GRP-001', { + page_num: 1, + page_size: 20, + total: 1, + }, { + next_v4_order_task_id: '9002', + next_v4_action_card_id: '9102', + next_v4_action_type: 'REVIEW', + next_v4_action_status: 'REVIEW_REQUIRED', + v4_open_order_task_count: 1, + })) + + const wrapper = await mountWithPlugins(ReservationOrderListView) + await vi.dynamicImportSettled() + + expect(wrapper.find('a[href="/reservation/order-tasks/9002"]').text()).toContain('继续处理') + expect(wrapper.text()).toContain('待复核') + }) + + it('falls back to the legacy task detail route when there is no V4 order task entrypoint', async () => { + vi.mocked(service.fetchReservationOrders).mockResolvedValue(createOrderListResult()) + + const wrapper = await mountWithPlugins(ReservationOrderListView) + await vi.dynamicImportSettled() + + expect(wrapper.find('a[href="/reservation/orders/20001"]').text()).toContain('查看详情') + expect(wrapper.find('a[href="/reservation/tasks/10002"]').text()).toContain('继续处理') + expect(wrapper.find('a[href="/reservation/order-tasks/9001"]').exists()).toBe(false) + }) + + it('shows no open task state when neither V4 nor legacy entrypoint exists', async () => { + vi.mocked(service.fetchReservationOrders).mockResolvedValue(createOrderListResult('GRP-001', { + page_num: 1, + page_size: 20, + total: 1, + }, { + open_task_count: 0, + next_processable_task_id: null, + next_v4_order_task_id: null, + next_v4_action_card_id: null, + next_v4_action_type: 'NONE', + next_v4_action_status: null, + v4_open_order_task_count: 0, + })) + + const wrapper = await mountWithPlugins(ReservationOrderListView) + await vi.dynamicImportSettled() + + expect(wrapper.find('a[href="/reservation/orders/20001"]').text()).toContain('查看详情') + expect(wrapper.find('a[href^="/reservation/tasks/"]').exists()).toBe(false) + expect(wrapper.find('a[href^="/reservation/order-tasks/"]').exists()).toBe(false) + expect(wrapper.text()).toContain('无待处理任务') + }) + it('filters order list with order status and business-key fields', async () => { vi.mocked(service.fetchReservationOrders).mockResolvedValue(createOrderListResult()) diff --git a/client/src/types/reservation.ts b/client/src/types/reservation.ts index 4004aca..507969b 100644 --- a/client/src/types/reservation.ts +++ b/client/src/types/reservation.ts @@ -58,6 +58,9 @@ export interface ReservationOrderListFilters { page_size?: number } +export type ReservationV4OrderListActionType = 'CONFIRM' | 'REVIEW' | 'NONE' +export type ReservationV4OrderListActionStatus = 'PENDING_CONFIRM' | 'REVIEW_REQUIRED' + export interface ReservationOrderListItem { order_id: string hotel_id: string @@ -69,6 +72,11 @@ export interface ReservationOrderListItem { display_name: string open_task_count: number | null next_processable_task_id: string | null + next_v4_order_task_id?: string | null + next_v4_action_card_id?: string | null + next_v4_action_type?: ReservationV4OrderListActionType | null + next_v4_action_status?: ReservationV4OrderListActionStatus | null + v4_open_order_task_count?: number | null created_at: string | null updated_at: string | null } diff --git a/client/src/views/reservation/ReservationOrderListView.vue b/client/src/views/reservation/ReservationOrderListView.vue index dbacdc7..cfe22a4 100644 --- a/client/src/views/reservation/ReservationOrderListView.vue +++ b/client/src/views/reservation/ReservationOrderListView.vue @@ -132,14 +132,20 @@ {{ t('orderList.viewDetail') }} {{ t('orderList.continueTask') }} + {{ formatContinueActionLabel(order) }} + + {{ t('orderList.noOpenTask') }} @@ -302,6 +308,29 @@ function formatOrderKey(order: ReservationOrderListItem): string { order.order_id ) } + +function resolveContinueTarget(order: ReservationOrderListItem): string | null { + if (order.next_v4_order_task_id) { + return `/reservation/order-tasks/${order.next_v4_order_task_id}` + } + if (order.next_processable_task_id) { + return `/reservation/tasks/${order.next_processable_task_id}` + } + return null +} + +function formatContinueActionLabel(order: ReservationOrderListItem): string { + if (!order.next_v4_order_task_id) { + return '' + } + if (order.next_v4_action_type === 'REVIEW' || order.next_v4_action_status === 'REVIEW_REQUIRED') { + return t('status.REVIEW_REQUIRED') + } + if (order.next_v4_action_type === 'CONFIRM' || order.next_v4_action_status === 'PENDING_CONFIRM') { + return t('status.PENDING_CONFIRM') + } + return '' +}