From 32bee57df61f74fa37a090afdb1516e77582a8e1 Mon Sep 17 00:00:00 2001 From: andy Date: Wed, 9 Sep 2026 15:47:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=B8=80=E5=AE=B6=E5=85=AC?= =?UTF-8?q?=E5=8F=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PROJECT_STATE.md | 17 +++++----- client/src/i18n/locales/en-US.ts | 1 + client/src/i18n/locales/th-TH.ts | 1 + client/src/i18n/locales/zh-CN.ts | 1 + client/src/tests/manualInvoiceView.spec.ts | 18 +++++++++++ .../ReservationManualInvoiceView.vue | 24 +++++++++++++- ...ai-native-software-engineering-standard.md | 31 +++++++++++++++---- .../ai-native-templates/SPEC.template.md | 28 +++++++++++------ .../general-development-guidelines.md | 8 ++++- .../backend-to-frontend-notes.md | 2 +- .../M009-manual-invoice-generation-v1.md | 4 ++- ...servationInvoiceGenerationServiceImpl.java | 28 +++++++++++++++++ 12 files changed, 136 insertions(+), 27 deletions(-) diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 32211cc..9dd2a01 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -2,18 +2,18 @@ | 项 | 内容 | | --- | --- | -| 最近更新 | 2026-07-24 | +| 最近更新 | 2026-09-09 | | 当前分支 | `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 收口、订单事项办理页克制业务办理台视觉 polish、SuperAgent MCP 入站诊断链路第一版、AI-NSES v0.2 / TH Hotel 项目级 Overlay 文档治理规则、M002 V4 增量需求模板化 Spec 对齐,以及单卡可操作态测试数据 smoke 回填 | +| 当前阶段 | 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 收口、订单事项办理页克制业务办理台视觉 polish、SuperAgent MCP 入站诊断链路第一版、AI-NSES v0.3 / TH Hotel 项目级 Overlay 文档治理规则、M002 V4 增量需求模板化 Spec 对齐,以及单卡可操作态测试数据 smoke 回填 | | 当前重点 | 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 -- 名称:`M002-V4-single-card-actionable-fixture-smoke-result-alignment` -- 状态:Done,测试机已造出 6 条 fresh V4 order task 数据,覆盖 Basic、Room Information、Trace General、Trace Extra Bed Review、Rooming List 和 Payment,目标卡均处于单卡可操作态;结果已回填到 `docs/project/requirements/M002-v4-requirement-spec-template-alignment.md`。 -- 目标:把测试 agent 的单卡可操作态造数结果、关键 ID、写操作范围和安全扫描结论落入文档,并确认 Payment 预览 / 下载时 DOM `img[src]` / `a[href]` 临时出现受权限附件 URL 的安全口径。 -- 边界:本 checkpoint 只改文档,不改业务代码、不改变 M002 V4 已实现接口、不改变 SuperAgent 入站 JSON、不改变权限、酒店隔离、审计或安全脱敏业务规则。 -- 联调备注:只执行造数和 5 次 Basic Information 前置确认;未确认目标业务卡、未执行 review-resolution、未 ack。V4 task detail API 和页面可见文本不得出现附件 URL;用户触发 Payment 预览 / 下载时,DOM `src/href` 临时持有 conversation 接口返回的受权限 URL 是允许行为。 +- 名称:`AI-NSES-progressive-complexity-principle` +- 状态:Done,通用 AI-NSES 标准已升为 v0.3,并加入“渐进式复杂度 / 第一版功能闭环优先 / 基础安全底线不可后置”的通用原则。 +- 目标:把“软件开发从简单到复杂,高级安全与高并发能力可以后续迭代,但基础安全底线必须从第一版保留”的确认结论落入可复用标准、通用开发规范和 Spec 模板。 +- 边界:本 checkpoint 只改通用文档和项目状态,不改业务代码、不改变 M002 V4 已实现接口、不改变前后端实现、不改变 SuperAgent / AgentBus 入站契约。 +- 联调备注:本 checkpoint 无运行时代码变化,无需测试机部署;验证以 Markdown diff 和 `git diff --check` 为准。 ## 2. 当前优先级 @@ -29,13 +29,14 @@ - AgentBus 是消息入口适配器,SuperAgent 是外部 AI / Agent 能力提供方,二者不能直接成为业务事实来源。 - 后端时间点按 UTC 存储和返回,页面再按酒店或用户时区展示。 - 新增 MySQL 表默认要求 `utf8mb4_bin`,避免外部 opaque id、Token、状态码和业务代码被大小写不敏感比较误判。 +- M009 手工开票公司候选已增加 `CENTURY_WAN_CHENG` / `Century Wan Cheng Tourism Co., Ltd.`,选择后自动回填 `Khun Nid` 的联系人资料;当前共八家公司,另保留 Manual 手工填写选项,完整资料见 M009 需求文档。现有接口、权限、生成流程和酒店税号不变。 ## 4. Known Issues - 现有历史文档暂不按 AI-NSES 目录大搬迁,先通过索引和采用说明建立映射关系。 - `docs/import/` 下按日期导入的资料是输入材料,不等同于当前权威开发契约;当前开发应优先看 `docs/project/README.md` 标记为当前有效或权威契约的文档。 - 后续每完成一个 Feature 或 Checkpoint,需要更新本文件,避免项目状态继续沉淀在聊天记录里。 -- AI-NSES v0.2 和 TH Hotel 项目级 Overlay 已落地;近期 M002 V4 新增需求已新增模板化 Spec 入口。后续新增或变更 V4 需求时必须继续维护该 Spec、后续 Change Request 或新 Spec,避免只散落在当前有效大文档中。 +- AI-NSES v0.3 和 TH Hotel 项目级 Overlay 已落地;v0.3 已加入渐进式复杂度原则,要求第一版优先功能闭环,高级安全 / 高并发 / 性能 / 可观测性可后续迭代,但 Secret 管理、基础权限边界、输入校验、敏感日志控制、数据隔离和错误响应脱敏必须从第一版保留。近期 M002 V4 新增需求已新增模板化 Spec 入口。后续新增或变更 V4 需求时必须继续维护该 Spec、后续 Change Request 或新 Spec,避免只散落在当前有效大文档中。 - M010 Rooming List Excel 生成后端 CP1 和前端 V1 已实现:前端 `/reservation/rooming-lists/new` 上传来源名单并下载后端同步生成的 `.xlsx`,第一版不落库、不上传 OSS。CP2 已实现:来源 Excel `旅游日期` 派生 Arrival / Departure,Adults 由系统按分房结果计算,目标默认值区域只保留 Payment Type / Nationality,Payment Type 默认 `BTQR` 且当前允许 `BTQR` / `CA`,Nationality 只允许 `KR` / `CN` / `TH` / `MM` / `RS` / `TW`。CP3 已实现:兼容第二种 `英文姓` + `英文名` 名单样式;CP4 已实现:兼容第三种单列 `英文名` 名单样式;CP5 已实现:兼容第四种 `拼音` + `分房` 原表分房样式,尊重来源表 `用房1间` 分房,`外住` 不进目标 Rooming List,`儿童情况=不占床` 进入 Accompanying Guests 但不计入 Adults。无旅游日期来源样式由用户补充 Arrival / Departure,且 `23+1`、`19+1` 中领队也进入房表。 - 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`;MCP `th_hotel_submit_task_results` 已收口为 M002 V4-only,旧 V2/V3 submit payload 返回 `MCP_SUBMIT_V4_REQUIRED`,不再影响 SuperAgent 输出契约;Room Information 后端展示模型和前端业务化展示第一版已完成;Rooming List 确认触发 Group Booking Status 自动置 `DEF` 已完成,前端轻量事项确认卡也已完成;Payment 附件安全摘要后端和前端预览接入均已完成。OWNER RATE Room Type / Rate Code 目录口径已落文档;真实 PMS 同步和 SuperAgent 目录机器接口仍未完成。 diff --git a/client/src/i18n/locales/en-US.ts b/client/src/i18n/locales/en-US.ts index 0f5e153..3047b5a 100644 --- a/client/src/i18n/locales/en-US.ts +++ b/client/src/i18n/locales/en-US.ts @@ -365,6 +365,7 @@ export default { noPdfUrl: 'The backend did not return a PDF URL.', backendTotals: 'Backend totals', validationTitle: 'Please fix the form', + generationErrorTitle: 'Generation failed', fieldErrors: { required: 'Required', invalidEmail: 'Invalid email', diff --git a/client/src/i18n/locales/th-TH.ts b/client/src/i18n/locales/th-TH.ts index 5952f3e..17d6c72 100644 --- a/client/src/i18n/locales/th-TH.ts +++ b/client/src/i18n/locales/th-TH.ts @@ -365,6 +365,7 @@ export default { noPdfUrl: 'หลังบ้านไม่ได้ส่งลิงก์ PDF กลับมา', backendTotals: 'ยอดจากหลังบ้าน', validationTitle: 'กรุณาแก้ไขแบบฟอร์ม', + generationErrorTitle: 'สร้างไม่สำเร็จ', fieldErrors: { required: 'จำเป็นต้องกรอก', invalidEmail: 'รูปแบบ Email ไม่ถูกต้อง', diff --git a/client/src/i18n/locales/zh-CN.ts b/client/src/i18n/locales/zh-CN.ts index b974615..aef6fc7 100644 --- a/client/src/i18n/locales/zh-CN.ts +++ b/client/src/i18n/locales/zh-CN.ts @@ -365,6 +365,7 @@ export default { noPdfUrl: '后端未返回 PDF 链接。', backendTotals: '后端返回金额', validationTitle: '请先修正表单', + generationErrorTitle: '生成失败', fieldErrors: { required: '必填', invalidEmail: 'Email 格式不正确', diff --git a/client/src/tests/manualInvoiceView.spec.ts b/client/src/tests/manualInvoiceView.spec.ts index 307e723..9b9e661 100644 --- a/client/src/tests/manualInvoiceView.spec.ts +++ b/client/src/tests/manualInvoiceView.spec.ts @@ -816,6 +816,24 @@ describe('ReservationManualInvoiceView', () => { expect(wrapper.text()).toContain('PDF 生成超时') }) + it('does not label backend generation failures as form validation errors', async () => { + vi.mocked(service.generateManualReservationInvoice).mockRejectedValue( + new ApiError('Request failed with status 502', 502, { + error_code: 'RESERVATION_INVOICE_GENERATION_FAILED', + message: 'Invoice PDF 生成失败,请稍后重试。', + }), + ) + const wrapper = mountView() + + await wrapper.find('[data-testid="recipient-company-code"]').setValue('QBD') + await fillMinimumInvoiceForm(wrapper) + await wrapper.find('[data-testid="manual-invoice-submit"]').trigger('click') + await flushPromises() + + expect(wrapper.text()).toContain('生成失败') + expect(wrapper.text()).not.toContain(zhCN.manualInvoice.validationTitle) + }) + it('keeps backend raw details in a folded technical detail block', async () => { vi.mocked(service.generateManualReservationInvoice).mockRejectedValue( new ApiError('Request failed with status 422', 422, { diff --git a/client/src/views/reservation/ReservationManualInvoiceView.vue b/client/src/views/reservation/ReservationManualInvoiceView.vue index c412340..35dc715 100644 --- a/client/src/views/reservation/ReservationManualInvoiceView.vue +++ b/client/src/views/reservation/ReservationManualInvoiceView.vue @@ -922,6 +922,19 @@ const recipientCompanies: RecipientCompanySeed[] = [ }, ], }, + { + company_code: 'CENTURY_WAN_CHENG', + company: 'Century Wan Cheng Tourism Co., Ltd.', + contacts: [ + { + contact_id: 'CENTURY_WAN_CHENG_KHUN_NID', + attention: 'Khun Nid', + address: '622 Soi Ratchada Niwet, Intersection 20, Huai Khwang District, Bangkok 10310', + telephone: '081-736-9808', + email: 'meojiutravel.hotel@gmail.com', + }, + ], + }, { company_code: 'HANATOUR', company: 'HANATOUR TD CO., LTD.', @@ -1015,6 +1028,7 @@ const messages = ref([]) const technicalDetails = ref([]) const downloadMessageKey = ref('') const alertTone = ref<'error' | 'success'>('error') +const alertKind = ref<'validation' | 'generation'>('validation') const generationResult = ref(null) const documentDatesEdited = ref(false) const primaryNightsEdited = ref(false) @@ -1044,7 +1058,9 @@ const alertTitle = computed(() => { if (alertTone.value === 'success') { return t('manualInvoice.resultSection') } - return t('manualInvoice.validationTitle') + return alertKind.value === 'generation' + ? t('manualInvoice.generationErrorTitle') + : t('manualInvoice.validationTitle') }) watch( @@ -1131,6 +1147,7 @@ async function submitInvoice(): Promise { validationAttempted.value = true messages.value = validateForm() alertTone.value = 'error' + alertKind.value = 'validation' if (messages.value.length > 0) { return } @@ -1144,6 +1161,7 @@ async function submitInvoice(): Promise { const errorDescription = describeGenerationError(error) messages.value = [errorDescription.message] technicalDetails.value = errorDescription.technicalDetails + alertKind.value = isBackendValidationError(error) ? 'validation' : 'generation' } finally { submitting.value = false } @@ -1457,6 +1475,10 @@ function errorCodeFromDetails(details: unknown): string { return 'UNKNOWN' } +function isBackendValidationError(error: unknown): boolean { + return error instanceof ApiError && errorCodeFromDetails(error.details) === 'RESERVATION_INVOICE_VALIDATION_FAILED' +} + function detailMessages(details: unknown): string[] { if (!isRecord(details)) { return [] diff --git a/docs/import/reusable/ai-native-software-engineering-standard.md b/docs/import/reusable/ai-native-software-engineering-standard.md index 651efcf..69a246f 100644 --- a/docs/import/reusable/ai-native-software-engineering-standard.md +++ b/docs/import/reusable/ai-native-software-engineering-standard.md @@ -2,7 +2,7 @@ | 项 | 内容 | | --- | --- | -| Version | 0.2 | +| Version | 0.3 | | Scope | 可复用软件工程标准 | | Audience | 人类开发者、产品人员、架构师、AI Agent | @@ -33,6 +33,12 @@ Prompt 是临时的,Context 是长期资产。重要知识应该沉淀在仓 文档不是一次性产物。文档随着项目成长,开发结束时文档也应该同步结束。 +### Progressive Complexity + +软件开发默认从简单到复杂。第一版应优先交付可运行、可验证的功能闭环,不为假想的未来复杂度提前引入过度抽象、复杂架构或难以理解的通用框架。 + +高级安全、高并发、性能优化、完整可观测性、复杂扩展性和容灾能力可以按风险和阶段后续迭代;但基础安全底线必须从第一版保留,包括 Secret 管理、基础权限边界、输入校验、敏感日志控制、数据隔离和错误响应脱敏。 + ### AI as Team Member AI 不是单纯的代码生成器,而是项目成员。它可以参与需求分析、产品设计、架构设计、开发、Review 和维护。 @@ -210,12 +216,24 @@ Idea Documentation Update 属于 Definition of Done,不能跳过。 -### 8.1 Definition of Ready +### 8.1 Version Scope and Progressive Hardening + +每个 Feature 或产品版本应显式区分: + +- 第一版必须完成的功能闭环。 +- 第一版必须保留的基础安全底线。 +- 本阶段明确不做的高级安全、高并发、性能、可观测性、扩展性或容灾能力。 +- 为后续迭代保留的低耦合边界和扩展点。 + +中文说明:第一版可以不追求完整平台化和高负载能力,但不能用“后续再做”为理由破坏基本安全、数据边界或模块职责。 + +### 8.2 Definition of Ready 进入实现前,复杂 Feature 或会影响跨模块协作的变更必须满足: - 需求来源明确,知道是谁提出、解决什么问题。 - 目标和非目标明确,避免实现时扩大范围。 +- 第一版范围、基础安全底线和后续加固项已经区分。 - 受影响的用户、业务流程、接口、数据模型、权限、安全、审计或外部系统边界已经列出。 - 前端、后端、测试或第三方系统的分工已经明确。 - 验收标准已经可测试,至少有主要 Given / When / Then 场景。 @@ -223,7 +241,7 @@ Documentation Update 属于 Definition of Done,不能跳过。 简单 bugfix 或纯内部重构可以不用新建完整 Spec,但仍应在任务说明中写清目标、范围和验证方式。 -### 8.2 Definition of Done +### 8.3 Definition of Done Feature 完成前必须确认: @@ -233,7 +251,7 @@ Feature 完成前必须确认: - 对外或跨团队契约发生变化时,调用方文档和测试说明已同步。 - 没有把 Secret、真实客户数据、构建产物、临时文件或无关本地变更纳入交付。 -### 8.3 Change Request +### 8.4 Change Request 当一个已 Approved 或 Implemented 的 Spec 发生需求变更时,优先新增或更新 Change Request,而不是把讨论散落到聊天记录中。 @@ -249,7 +267,7 @@ Change Request 至少说明: 小变更可以直接追加到原 Spec 的“变更记录”章节;跨前后端、权限、安全、数据模型或第三方契约的变更应单独成文。 -### 8.4 Traceability Matrix +### 8.5 Traceability Matrix 复杂 Feature 应维护需求追踪表,用于连接需求、实现、测试和文档。 @@ -261,7 +279,7 @@ Change Request 至少说明: 追踪表可以放在 Spec、Project State 或专项 checkpoint 文档中;关键是让新 Agent 能快速判断“文档写了但代码没做、代码做了但文档没写、后端做了但前端没做、前端需要但后端没做”。 -### 8.5 Agent Handoff +### 8.6 Agent Handoff 给 AI Agent 分派任务时,建议使用统一交接结构: @@ -281,6 +299,7 @@ Change Request 至少说明: - 复杂功能默认先 Spec,不直接实现。 - Bug 默认先定位,不猜测修复。 - UI 默认保持一致性,不过度设计。 +- 第一版默认先实现可验证的功能闭环,避免提前设计复杂平台化能力;高级安全和高并发能力可以后续迭代,但基础安全底线必须从第一版保留。 - 代码修改前先确认目标、边界和验收标准。 - 涉及接口、安全、权限、数据模型或外部系统时,先读相关契约文档。 - 涉及跨 agent 协作时,先确认 Spec、Change Request 或 handoff 是否足够清楚。 diff --git a/docs/import/reusable/ai-native-templates/SPEC.template.md b/docs/import/reusable/ai-native-templates/SPEC.template.md index 68478f6..0af6038 100644 --- a/docs/import/reusable/ai-native-templates/SPEC.template.md +++ b/docs/import/reusable/ai-native-templates/SPEC.template.md @@ -22,54 +22,64 @@ - 不做事项 1: - 不做事项 2: -## 4. 用户与场景 +## 4. 第一版范围与后续加固 + +- 第一版必须完成的功能闭环: +- 第一版必须保留的基础安全底线:Secret 管理 / 基础权限边界 / 输入校验 / 敏感日志控制 / 数据隔离 / 错误响应脱敏 +- 本阶段后置的高级安全、高并发、性能、完整可观测性、复杂扩展性或容灾能力: +- 为后续迭代保留的低耦合边界和扩展点: + +## 5. 用户与场景 说明谁会使用这个能力,在哪些场景使用。 -## 5. Definition of Ready +## 6. Definition of Ready - 需求来源已确认: - 目标和非目标已确认: +- 第一版范围、基础安全底线和后续加固项已确认: - 影响范围已确认: - 权限、安全、审计和数据边界已确认: - 前后端 / 测试分工已确认: - 未确认问题已列出: -## 6. 业务规则 +## 7. 业务规则 - 规则 1: - 规则 2: -## 7. 接口或交互契约 +## 8. 接口或交互契约 说明请求、响应、权限、安全、审计和兼容性要求。 -## 8. 需求追踪表 +## 9. 需求追踪表 | 需求项 | 后端状态 | 前端状态 | 测试状态 | 文档位置 | 当前状态 | | --- | --- | --- | --- | --- | --- | | 需求 1 | Pending / Done / N/A | Pending / Done / N/A | Pending / Done / Blocked | | Draft / Approved / Implemented | -## 9. 验收标准 +## 10. 验收标准 - Given / When / Then: - Given / When / Then: -## 10. 测试范围 +## 11. 测试范围 - 单元测试: - 集成测试: - 手工验证: +- 本阶段未覆盖的高并发、安全加固、性能或可观测性验证: -## 11. Definition of Done +## 12. Definition of Done - 实现满足 Spec: - 测试已运行或说明无法运行原因: - 需求追踪表已更新: - Project State 已更新: - 接口、安全、权限、审计、集成契约已同步: +- 后续加固项已记录,不被误标为已完成: - 无 Secret、真实数据、构建产物或无关本地变更: -## 12. 文档更新 +## 13. 文档更新 完成后检查 Domain、Architecture、Workflow、ADR、Project State 是否需要更新。 diff --git a/docs/import/reusable/general-development-guidelines.md b/docs/import/reusable/general-development-guidelines.md index 0c48229..a42b654 100644 --- a/docs/import/reusable/general-development-guidelines.md +++ b/docs/import/reusable/general-development-guidelines.md @@ -16,6 +16,9 @@ - 遇到不确定的技术栈、目录、接口契约或数据模型,先确认再继续。 - 新项目初始化或技术栈升级前,必须检查前端、后端、构建工具、测试工具和运行时版本兼容性。 - 输出分层结构、目录树、数据模型、字段映射或接口示例时,必须补充中文说明,不能只依赖英文命名表达业务含义。 +- 软件开发默认从简单到复杂;第一版优先实现可运行、可验证的功能闭环,不为假想未来提前引入复杂架构或大而全抽象。 +- 高级安全、高并发、性能、完整可观测性和复杂扩展性可以后续迭代;基础安全底线必须从第一版保留,包括 Secret 管理、基础权限边界、输入校验、敏感日志控制、数据隔离和错误响应脱敏。 +- 如果把某些加固能力后置,必须在 Spec、Project State 或任务输出中明确记录,不能让后续 agent 误判为已经完成。 ## 3. 分支与提交 @@ -57,6 +60,8 @@ 项目各功能模块必须保持低耦合、高内聚和高可维护性。新增功能时,应先明确模块边界、输入输出、依赖方向和验收标准,再开始实现。 +第一版实现应保持结构清晰但不过度设计:只抽取已经有真实重复或稳定语义的公共能力,不为了“未来可能需要”提前建立复杂框架。后续扩展点应通过清晰模块边界、接口契约和测试保护,而不是通过难以理解的通用模型堆叠。 + 落地要求: - 每个模块只负责一个清晰业务能力。 @@ -86,7 +91,7 @@ - 外部系统通过 Port / Adapter 隔离,业务层不直接依赖厂商 SDK 或外部 DTO。 - 数据库变更必须可追踪;已发布 migration 不直接修改。 - 新项目建表前必须明确数据库字符集和 collation 策略;MySQL 项目默认建议使用 `utf8mb4_bin`,避免外部 ID、Token、哈希、状态码或业务代码因大小写不敏感而误判。 -- 写接口要考虑幂等、并发版本、审计、失败恢复和脱敏。 +- 第一版写接口必须保留输入校验、权限边界、错误脱敏和 Secret 不泄露;幂等、并发版本、审计、失败恢复等能力按业务风险决定当前实现深度,并在后置项中记录未完成边界。 Java 后端代码默认参考 Alibaba Java Coding Guidelines。可复用摘要见 `docs/import/reusable/alibaba-java-coding-guidelines-summary.md`。 @@ -124,6 +129,7 @@ Java 后端代码默认参考 Alibaba Java Coding Guidelines。可复用摘要 - 不假装测试通过。 - 优先做小的可验证闭环,再逐步扩展业务能力。 - 高风险改动需要补充更接近真实使用路径的测试。 +- 如果本版本只覆盖功能闭环,必须说明尚未覆盖的高并发、安全加固、性能或可观测性验证范围。 ## 12. Agent 协作规则 diff --git a/docs/project/frontend-backend/backend-to-frontend-notes.md b/docs/project/frontend-backend/backend-to-frontend-notes.md index b48d173..8acef2d 100644 --- a/docs/project/frontend-backend/backend-to-frontend-notes.md +++ b/docs/project/frontend-backend/backend-to-frontend-notes.md @@ -448,7 +448,7 @@ M009 后端 CP2 已实现:页面可不依赖订单或任务,用户手工填 - 从任务进入时,后续可以使用 `GET /api/reservation/tasks/{taskId}` 的 `fields[]`、草稿或确认 payload 预填;从订单进入时,需要明确选择具体任务或提示仅使用订单摘要,避免一个订单多任务时字段来源不清。 - Company、Attention、Address、Tel、Email、Booking Date 第一阶段可以按“选择 + Manual 手填”控件处理。 - Company、Attention、Address、Tel、Email 是一组收件方联系人档案,不是五个互相独立字段;选择 Company 后应刷新 Attention 候选,选择 Attention 后应带出 Address、Tel、Email。Booking Date 展示在同一区域,但不属于联系人档案,应作为 `document.booking_date` 独立提交。 -- 第一阶段已确认七组客户 / 旅行社种子数据:`LIAN_TAI` / `LIAN TAI TRAVEL (THAILAND) CO., LTD.` + `Khun Ann`;`QBD` / `Q.B.D. TRAVEL GROUP CO., LTD` + `Jitdanun Panaphuchong`;`MING_SONG` / `MING SONG CO.LTD` + `KHUN JIN`;`TEIGER_TRAVEL` / `TEIGER TRAVEL GROUP CO.,LTD` + `Khun Nutkritta Upamai (Kwan)`;`CLOVER_HOLIDAY` / `CLOVER HOLIDAY` + `KHUN JANE`;`KANGLER_TRAVEL` / `KANGLER TRAVEL GROUP CO.,LTD` + `Khun Dow`;`HANATOUR` / `HANATOUR TD CO., LTD.` + 7 个联系人。完整数据以 `docs/project/requirements/M009-manual-invoice-generation-v1.md` 为准。 +- 第一阶段已确认八组客户 / 旅行社种子数据:`LIAN_TAI` / `LIAN TAI TRAVEL (THAILAND) CO., LTD.` + `Khun Ann`;`QBD` / `Q.B.D. TRAVEL GROUP CO., LTD` + `Jitdanun Panaphuchong`;`MING_SONG` / `MING SONG CO.LTD` + `KHUN JIN`;`TEIGER_TRAVEL` / `TEIGER TRAVEL GROUP CO.,LTD` + `Khun Nutkritta Upamai (Kwan)`;`CLOVER_HOLIDAY` / `CLOVER HOLIDAY` + `KHUN JANE`;`KANGLER_TRAVEL` / `KANGLER TRAVEL GROUP CO.,LTD` + `Khun Dow`;`CENTURY_WAN_CHENG` / `Century Wan Cheng Tourism Co., Ltd.` + `Khun Nid`;`HANATOUR` / `HANATOUR TD CO., LTD.` + 7 个联系人。另保留 Manual 手工填写选项;完整数据以 `docs/project/requirements/M009-manual-invoice-generation-v1.md` 为准。 - Room Type、Room Rate、Extra Bed 建议也预留“选择 + 可手填 / 可覆盖”能力,后续接 PMS 房型、Rate Code 或价格配置。 - Room Rate / 费用明细 Rate 第一阶段只校验必填、数字格式和非负数,允许 `0`;Quantity 和 Nights 仍必须大于 `0`。 - No.of Night(s) 前端默认按 `Departure Date - Arrival Date` 自动填入第一条费用明细 Night(s),用户手工修改后保留用户输入,不再因日期变化覆盖。 diff --git a/docs/project/requirements/M009-manual-invoice-generation-v1.md b/docs/project/requirements/M009-manual-invoice-generation-v1.md index 66cf67e..b6cd8b7 100644 --- a/docs/project/requirements/M009-manual-invoice-generation-v1.md +++ b/docs/project/requirements/M009-manual-invoice-generation-v1.md @@ -107,7 +107,7 @@ - `Address` 可在公司级和联系人级之间复用;如果同一公司多个联系人共用地址,不应在每个联系人手工重复维护多份无关数据。 - 用户选择 `Manual` 时,才允许脱离目录手工填写;提交时仍保存最终文本值,目录 code 仅用于审计和后续追溯。 -第一阶段确认的种子数据: +第一阶段确认的种子数据(当前共八家公司,另保留 Manual 手工填写选项): | company_code | Company | contact_id | Attention | Address | Tel | Email | | --- | --- | --- | --- | --- | --- | --- | @@ -117,6 +117,7 @@ | `TEIGER_TRAVEL` | `TEIGER TRAVEL GROUP CO.,LTD` | `TEIGER_TRAVEL_KHUN_NUTKRITTA_UPAMAI` | `Khun Nutkritta Upamai (Kwan)` | `20,22 Soi Rinrada,Srinakarin, Pattanakarn,Bangkok,TH, 10250` | `063-516-8294` | `nutkritta.upamai@gmail.com` | | `CLOVER_HOLIDAY` | `CLOVER HOLIDAY` | `CLOVER_HOLIDAY_KHUN_JANE` | `KHUN JANE` | `290 SOI Lat Phrao 94 (Panjamit), Phlapphla Subdistrict, 555,555/1 Moo 1, Na Jomtien, Sattahip` | `083-1470352` | `longtai.maleemalee@gmail.com` | | `KANGLER_TRAVEL` | `KANGLER TRAVEL GROUP CO.,LTD` | `KANGLER_TRAVEL_KHUN_DOW` | `Khun Dow` | `เลขที่290/27, Soi Ladprao 84, Wangthonglang,Bangkok,10310` | `081-1237170` | `kanglertravel.op91@gmail.com` | +| `CENTURY_WAN_CHENG` | `Century Wan Cheng Tourism Co., Ltd.` | `CENTURY_WAN_CHENG_KHUN_NID` | `Khun Nid` | `622 Soi Ratchada Niwet, Intersection 20, Huai Khwang District, Bangkok 10310` | `081-736-9808` | `meojiutravel.hotel@gmail.com` | | `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_WICHIENPRAKARN` | `Wichienprakarn, Nuanphae, Khun.` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `066 124 - 7297` | `HI219@hanatour.com` | | `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_PHONGBUPPA` | `Phongbuppa, Buppachat` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `096 051 3587` | `HI223@hanatour.com` | | `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_KANG_SUNG_HWA` | `Kang,Sung Hwa` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `82 051 804 0707` | `k8040707@nave.com` | @@ -130,6 +131,7 @@ - 以上数据第一阶段可以作为后端种子配置、数据库初始化数据或前端原型静态数据,但正式业务接口生成 PDF 时应以请求中的最终文本值为准。 - 后续如果接客户 / 联系人维护后台,`company_code` 和 `contact_id` 应保持稳定,不使用 Company 或 Attention 展示文案做业务主键。 - Booking Date 虽然展示在 Basic Information 区域,但它不是联系人档案的一部分,应独立保存在 `invoice_payload.document.booking_date`。 +- 2026-09-09 按用户提供的样张新增 `CENTURY_WAN_CHENG`,沿用选择公司后自动回填联系人资料、允许人工覆盖的流程。样张中的客户 Tax ID 为 `0105568097215`,当前 Recipient 请求结构未提供客户税号字段,先作为资料记录,不写入酒店 Tax ID;样张中的 Booking Date / Booking No. 不作为公司默认值。本次仅扩充现有公司候选,未引入新 UI 或设计 skill;手工开票页面和服务的已有 30 项测试、前端类型检查、构建及页面 lint 已通过。Domain、Architecture、Workflow、ADR 和安全边界文档无需变更。 ### 4.2 Reservation diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationInvoiceGenerationServiceImpl.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationInvoiceGenerationServiceImpl.java index c935e46..55e1386 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationInvoiceGenerationServiceImpl.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationInvoiceGenerationServiceImpl.java @@ -36,6 +36,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; @@ -46,6 +48,7 @@ import org.springframework.stereotype.Service; @Service public class ReservationInvoiceGenerationServiceImpl implements ReservationInvoiceGenerationService { + private static final Logger log = LoggerFactory.getLogger(ReservationInvoiceGenerationServiceImpl.class); private static final String DEFAULT_TEMPLATE_CODE = "PROFORMA_INVOICE_V1"; private static final String TEMPLATE_VERSION = "v1"; private static final String CURRENCY = "THB"; @@ -137,6 +140,7 @@ public class ReservationInvoiceGenerationServiceImpl implements ReservationInvoi exception.getErrorCode(), safeSummary(exception.getMessage()), nowUtc()); + logInvoiceGenerationFailure(generationId, hotelId, exception.getErrorCode(), exception.getMessage(), exception); throw exception; } catch (DocumentConversionException exception) { invoiceGenerationRepository.markFailed( @@ -144,6 +148,7 @@ public class ReservationInvoiceGenerationServiceImpl implements ReservationInvoi exception.getErrorCode(), safeSummary(exception.getMessage()), nowUtc()); + logInvoiceGenerationFailure(generationId, hotelId, exception.getErrorCode(), exception.getMessage(), exception); throw new ReservationInvoiceGenerationException( exception.getStatus(), exception.getErrorCode(), @@ -154,6 +159,12 @@ public class ReservationInvoiceGenerationServiceImpl implements ReservationInvoi "RESERVATION_INVOICE_GENERATION_FAILED", safeSummary(exception.getMessage()), nowUtc()); + logInvoiceGenerationFailure( + generationId, + hotelId, + "RESERVATION_INVOICE_GENERATION_FAILED", + exception.getMessage(), + exception); throw new ReservationInvoiceGenerationException( HttpStatus.BAD_GATEWAY, "RESERVATION_INVOICE_GENERATION_FAILED", @@ -528,6 +539,23 @@ public class ReservationInvoiceGenerationServiceImpl implements ReservationInvoi return normalized.length() <= 500 ? normalized : normalized.substring(0, 500); } + private void logInvoiceGenerationFailure( + Long generationId, + String hotelId, + String safeErrorCode, + String message, + RuntimeException exception) { + log.warn( + "Reservation Invoice generation failed. generation_id={}, hotel_id={}, safe_error_code={}, " + + "safe_error_summary={}, exception_class={}", + generationId, + hotelId, + safeErrorCode, + safeSummary(message), + exception.getClass().getSimpleName(), + exception); + } + private String safeFileName(String fileName, String fallback) { String value = trimToNull(fileName); if (value == null) {