实现手工发票生成后端接口

This commit is contained in:
andy
2026-07-17 11:24:51 +07:00
parent bc0413f21c
commit 11478c6913
35 changed files with 2416 additions and 2 deletions

View File

@@ -0,0 +1,401 @@
# M009 Manual Invoice 手工开票生成 V1
| 项目 | 内容 |
| --- | --- |
| 文档状态 | CP2 后端已实现,前端 CP3 待开发 |
| 适用范围 | 无订单 / 无任务场景下的手工 Invoice 创建,以及后续从任务或订单预填 Invoice |
| 当前目标 | 第一阶段基于现有 `invoice.html` 原型落地 Manual Invoice Mode后端已支持手工生成 PDF前端页面待接入 |
| 依赖能力 | M008 Excel 转 PDF 平台转换能力、阿里云 OSS、登录权限和酒店上下文 |
## 1. 背景
当前系统的主业务数据来自 AgentBus、SuperAgent、订单和任务。但实际运营中可能出现极端情况
- 邮件还没有被 SuperAgent 正确识别成订单 / 任务。
- 订单或任务尚未入库,用户仍需要先出一张 Proforma Invoice。
- 历史、线下、电话或临时沟通产生的开票需求没有可关联的任务。
- SuperAgent 或主链路故障时,需要人工兜底生成 PDF。
因此第一阶段需要支持一个独立的 `Manual Invoice Mode`:用户可以不选择订单、不选择任务,直接填写或选择 Invoice 字段,由后端生成 Excel再通过 LibreOffice 转为 PDF 并上传 OSS。
## 2. 第一阶段定位
第一阶段不是完整财务发票系统,而是酒店预订业务中的 Proforma Invoice 生成能力。
第一阶段应做到:
- 页面可从独立入口进入,例如 `/reservation/invoices/new``/invoices/new`
- 不要求 `task_id``order_id`
- 用户手工填写 / 选择字段后,后端生成 PDF。
- 后端重新计算金额、税额和合计,不信任前端提交的计算结果。
- 酒店、法人、税号、银行账户、Logo 和固定文案优先来自模板或酒店发票配置。
- 生成行为必须有权限、酒店隔离和审计记录。
第一阶段不做:
- 不接真实 OPERA / OHIP。
- 不要求必须挂靠订单或任务。
- 不做应收账款、收款核销、发票税务编号或财务系统入账。
- 不把 SuperAgent AI 原始 JSON 直接渲染成 Invoice 表单。
- 不允许前端直接调用 M008 调试上传接口来伪造业务开票。
## 3. 与现有流程关系
### 3.1 手工模式
```text
用户打开手工 Invoice 页面
-> 前端读取发票表单配置和默认值
-> 用户填写 / 选择字段
-> 前端提交 invoice_payload
-> 后端校验、计算金额和税额
-> 后端填充 Excel 模板
-> 调用 M008 内部转换能力生成 PDF
-> PDF 上传 OSS
-> 写入 Invoice 生成记录和审计
-> 返回 PDF URL 和生成摘要
```
### 3.2 任务或订单预填模式
后续可以支持从任务或订单进入:
```text
任务详情 / 订单详情
-> 打开 Invoice 页面并带 task_id 或 order_id
-> 后端按 task_id / order_id 查询可用上下文
-> 返回同一套 invoice_payload 默认值
-> 用户确认或修改
-> 后续生成流程与手工模式相同
```
中文说明:
- `task_id``order_id` 只是可选上下文,不是第一阶段生成 Invoice 的必要条件。
- 如果从任务进入,应优先使用任务详情 `fields[]``draft_payload_json``confirmed_payload_json` 中已确认的数据。
- 如果从订单进入,应由用户选择具体任务或由页面显式展示“仅使用订单摘要生成”,避免一个订单多任务时字段来源不清。
## 4. 字段来源和控件分类
基于现有 `invoice.html` 原型和 Excel 模板,字段分为四类。
| 分类 | 中文说明 | 示例字段 | 第一阶段控件建议 |
| --- | --- | --- | --- |
| 用户输入字段 | 每张 Invoice 都可能不同,需要用户填写或修改 | Group Name、Arrival Date、Departure Date、Due Date、Room Rate Note、明细 Description | `input` / `date` / `textarea` |
| 选择 + 可手填字段 | 有标准目录,但业务上允许人工覆盖 | Company、Attention、Address、Tel、Email、Booking Date、Room Type、Room Rate、Extra Bed | `select + Manual` 或后续 combobox |
| 模板 / 配置字段 | 不是每张 Invoice 的业务输入,随酒店、法人或模板版本变化 | 酒店名称、Tax ID、法人公司、银行账号、Logo、固定付款文案、页脚 | 第一阶段可在模板或酒店发票配置中维护 |
| 系统计算字段 | 不应由用户手填,后端或 Excel 公式计算 | Amount、Sub-Total、VAT、Total、Night(s) 汇总、Room(s) 汇总 | 前端只读预览,后端重新计算 |
### 4.1 Basic Information
| 字段 | 控件 | 后续来源 |
| --- | --- | --- |
| Company | 选择 + 可手填 | 客户 / 旅行社目录,第一阶段可使用后端配置或静态种子 |
| Attention | 选择 + 可手填 | Company 下联系人目录 |
| Address | 选择 + 可手填 | 联系人或公司账单地址 |
| Tel | 选择 + 可手填 | 联系人电话 |
| Email | 选择 + 可手填 | 联系人邮箱 |
| Booking Date | 选择 + 可手填 | 默认使用当前酒店本地日期,也可用户调整 |
### 4.1.1 Recipient Directory 第一阶段种子数据
`Company``Attention``Address``Tel``Email` 不是五个互相独立的普通字段,而是一组收件方 / 旅行社联系人档案。第一阶段页面可以允许 Manual 覆盖,但默认选择逻辑必须保持关联关系:
- 选择 `Company` 后,刷新该公司下可选 `Attention` 列表。
- 选择 `Attention` 后,同步带出该联系人的 `Address``Tel``Email`
- 公司只有一个联系人时,可以默认选中该联系人并自动带出地址、电话和邮箱。
- `Address` 可在公司级和联系人级之间复用;如果同一公司多个联系人共用地址,不应在每个联系人手工重复维护多份无关数据。
- 用户选择 `Manual` 时,才允许脱离目录手工填写;提交时仍保存最终文本值,目录 code 仅用于审计和后续追溯。
第一阶段确认的种子数据:
| company_code | Company | contact_id | Attention | Address | Tel | Email |
| --- | --- | --- | --- | --- | --- | --- |
| `LIAN_TAI` | `LIAN TAI TRAVEL (THAILAND) CO., LTD.` | `LIAN_TAI_KHUN_ANN` | `Khun Ann` | `2/86 Rajpattana Road, Rajpattana, Sapansoong, Bangkok, TH, 10240` | `061-397-2675` | `op.liantaitravel@gmail.com` |
| `QBD` | `Q.B.D. TRAVEL GROUP CO., LTD` | `QBD_JITDANUN_PANAPHUCHONG` | `Jitdanun Panaphuchong` | `2/90 Rajpattana,Rajpattana,Sapansoong, Bangkok, TH, 10240` | `089-032 0176` | `op.qbdtravel@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` |
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_MINYOUNG_KIM` | `Minyoung Kim` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `82 010 6638 3345` | `mykim1220@hanayour.com` |
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_BOLAM_JO` | `Bolam Jo` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `82 010 4182 4615` | `melissa0609@hanatour.com` |
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_HWANG_SEONGSEOP` | `Hwang Seongseop` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `82 010 3167 8648` | `hwangpilot59@naver.com` |
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_SONG_SAE_HWA` | `Song Sae Hwa` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `82 010 2400 6801` | `sh-tour2016@naver.com` |
中文说明:
- 以上数据第一阶段可以作为后端种子配置、数据库初始化数据或前端原型静态数据,但正式业务接口生成 PDF 时应以请求中的最终文本值为准。
- 后续如果接客户 / 联系人维护后台,`company_code``contact_id` 应保持稳定,不使用 Company 或 Attention 展示文案做业务主键。
- Booking Date 虽然展示在 Basic Information 区域,但它不是联系人档案的一部分,应独立保存在 `invoice_payload.document.booking_date`
### 4.2 Reservation
| 字段 | 控件 | 后续来源 |
| --- | --- | --- |
| Group Name / Group Code | 输入框 | 手工填写;任务预填时来自 `case_keys.group_code` 或订单 `group_code` |
| Arrival Date | 日期输入 | 手工填写;任务预填时来自任务字段 |
| Departure Date | 日期输入 | 手工填写;任务预填时来自任务字段 |
| Due Date | 日期输入 | 手工填写;可按酒店付款规则默认生成 |
| Room Rate | 选择 + 可手填 | 第一阶段可手填;后续接 Rate Code / 房价配置 |
| Room Rate Note | 输入框 | 例如 `includingBF` |
| Extra bed | 选择 + 可手填 | 第一阶段可手填;后续接加床价格配置 |
| No. of Room(s) | 输入框 / 只读派生 | 可由第一条明细同步,也允许手工覆盖 |
| No. of Night(s) | 输入框 / 日期派生 | 可由 Arrival / Departure 派生,也允许手工覆盖 |
### 4.3 Description & Amount
| 字段 | 控件 | 后续来源 |
| --- | --- | --- |
| Description | 输入框 | 手工填写;默认可使用 Group Name 或 Booking No |
| Room Types | 选择 + 可手填 | 第一阶段可手填;后续接 PMS 房型目录 |
| Quantity(s) | 数字输入 | 后端要求正数 |
| Rate | 数字输入 | 后端要求正数 |
| Night(s) | 数字输入 | 后端要求正数 |
| Amount | 只读 | `quantity * rate * nights` |
| Sub-Total | 只读 | 后端按 VAT 规则计算 |
| VAT 7% | 只读 | 后端按配置税率计算 |
| Total amount | 只读 | 明细金额合计 |
## 5. 建议请求模型
第一阶段建议新增业务接口,不复用 M008 的调试上传接口:
```text
POST /api/reservation/invoices/manual-generations
Authorization: Bearer <access_token>
Content-Type: application/json
```
请求体草案:
```json
{
"hotel_id": "HOTEL-TEST",
"source_type": "MANUAL",
"task_id": null,
"order_id": null,
"template_code": "PROFORMA_INVOICE_V1",
"invoice_payload": {
"document": {
"invoice_date": "2026-07-17",
"booking_date": "2026-07-12",
"due_date": "2026-07-22"
},
"recipient": {
"company_code": "LIAN_TAI",
"contact_id": "LIAN_TAI_KHUN_ANN",
"company": "LIAN TAI TRAVEL (THAILAND) CO., LTD.",
"attention": "Khun Ann",
"address": "2/86 Rajpattana Road, Rajpattana, Sapansoong, Bangkok, TH, 10240",
"telephone": "061-397-2675",
"email": "op.liantaitravel@gmail.com"
},
"booking": {
"group_name": "GRP-DEMO-0802",
"arrival_date": "2026-08-02",
"departure_date": "2026-08-05",
"room_rate_note": "includingBF",
"extra_bed_rate": 1200
},
"charges": [
{
"description": "GRP-DEMO-0802",
"room_type": "Deluxe Room",
"quantity": 2,
"rate": 3000,
"nights": 3
}
]
}
}
```
中文说明:
- `source_type=MANUAL` 表示无订单 / 无任务的手工开票。
- `task_id``order_id` 第一阶段允许为空。
- 如果同时传入 `task_id``order_id`,后端会校验该任务必须挂靠在该订单下;不一致时返回 `RESERVATION_INVOICE_CONTEXT_MISMATCH`
- `invoice_payload` 中的日期是酒店本地业务日期,使用 `yyyy-MM-dd`
- 前端可以传金额预览,但后端不得信任;最终金额以服务端计算和模板公式为准。
- 客户目录选择码和手填文本可以同时传;后端第一阶段以文本值生成 PDF`company_code``contact_id` 用于审计和后续配置追溯。
成功响应草案:
```json
{
"invoice_generation_id": "2080000000000000001",
"generation_status": "SUCCEEDED",
"source_type": "MANUAL",
"hotel_id": "HOTEL-TEST",
"template_code": "PROFORMA_INVOICE_V1",
"pdf_url": "https://oss.example.test/reservation-invoices/HOTEL-TEST/2026-07-17/.../proforma-invoice.pdf",
"pdf_object_key": "reservation-invoices/HOTEL-TEST/2026-07-17/.../proforma-invoice.pdf",
"generated_excel_object_key": "reservation-invoices/HOTEL-TEST/2026-07-17/.../proforma-invoice.xlsx",
"totals": {
"subtotal": 16822.43,
"vat": 1177.57,
"total": 18000.00,
"currency": "THB"
},
"created_at": "2026-07-17T03:30:00Z"
}
```
## 6. 建议数据模型
第一阶段建议持久化生成记录,便于审计、排查和后续列表查询。
建议新增表:
```text
workflow_reservation_invoice_generation
```
核心字段:
| 字段 | 中文含义 |
| --- | --- |
| `id` | Invoice 生成记录 ID |
| `hotel_id` | 酒店 ID |
| `source_type` | 来源类型:`MANUAL``TASK``ORDER` |
| `order_id` | 可选订单 ID |
| `task_id` | 可选任务 ID |
| `source_message_id` | 可选来源邮件 ID |
| `template_code` | 模板编码 |
| `template_version` | 模板版本 |
| `invoice_payload_json` | 用户提交并经后端归一化后的业务字段 |
| `calculated_totals_json` | 后端计算后的金额、税额和合计 |
| `generated_excel_object_key` | 生成后的 Excel OSS 对象 Key |
| `pdf_object_key` | PDF OSS 对象 Key |
| `pdf_url` | PDF 访问 URL |
| `generation_status` | 生成状态 |
| `safe_error_code` | 安全错误码 |
| `safe_error_summary` | 安全错误摘要 |
| `created_by` | 创建人 |
| `created_at` / `updated_at` | UTC 时间点 |
中文说明:
- `generated_excel_object_key``pdf_object_key``pdf_url` 不只在成功记录中出现;如果流程在后续步骤失败,后端也会保留已经成功上传的对象定位,避免 OSS 生成物变成不可追踪的孤立文件。
- `SUCCEEDED` 只在模板填充、Excel 上传、PDF 转换、PDF 上传和业务审计均完成后写入;如果业务审计失败,不会先标记成功再覆盖为失败。
建议状态:
| 状态 | 中文含义 |
| --- | --- |
| `PENDING` | 已接收,等待生成 |
| `RUNNING` | 正在填模板或转换 |
| `SUCCEEDED` | 生成成功 |
| `FAILED` | 生成失败 |
第一阶段也可以同步生成并直接返回,但仍建议落库,因为这属于业务文档生成,不是一次性调试转换。
## 7. 后端模块边界
建议放在 Reservation 工作流下,而不是平台文件转换模块:
```text
server/src/main/java/cn/nianxx/thhotel/workflows/reservation/invoice
├── control
├── service
│ └── impl
├── domain
├── mapper
├── repository
└── common
├── dto
├── request
├── result
└── enums
```
中文说明:
- `reservation.invoice` 负责业务字段校验、模板字段映射、发票生成记录、权限和审计。
- `platform.documentconversion` 继续作为底层文件转换能力,负责 Excel 到 PDF。
- `integrations.storage.aliyunoss` 继续作为 OSS 能力。
- 前端不能直接调用 `POST /api/system/document-conversions/excel-to-pdf` 来完成业务开票;那是调试 / 后台工具接口。
## 8. 模板和字段映射
第一阶段建议先使用一个受控模板:
```text
server/src/main/resources/templates/reservation-invoice/proforma-invoice-v1.xlsx
```
后续可迁移到 OSS + 数据库模板版本管理。
模板字段映射原则:
- 模板固定文案、Logo、银行信息、税号可以先保留在模板内。
- 可动态变更但不属于单张 Invoice 的配置,后续迁移到酒店发票配置。
- 用户输入字段通过后端模板填充器写入指定单元格。
- 明细行需要支持 1 到 N 行;超过模板默认行数时,后端应复制行样式和公式。
- 金额公式由后端统一写入或由模板公式统一生成,不复制历史案例中不一致的公式。
## 9. 权限、安全和审计
第一阶段建议:
- 接口分类:`FRONTEND_USER`,不是 `FRONTEND_DEBUG`
- 必须 Bearer 登录。
- 权限码建议:`RESERVATION_INVOICE_GENERATE`
- 必须校验用户对 `hotel_id` 的访问权。
- 如果传入 `task_id``order_id`,必须反查对象所属酒店并校验与 `hotel_id` 一致;如果两者同时传入,必须校验任务所属订单与 `order_id` 一致。
- 写入业务审计,记录创建人、酒店、来源类型、可选订单 / 任务、模板版本和生成结果。
- 日志不得输出完整 `invoice_payload_json`、客户邮箱、电话、OSS 签名 URL 或 PDF 内容。
## 10. Checkpoint 规划
### CP1文档和页面方向确认
- 落地本文档。
- 明确当前 `invoice.html` 原型可作为前端交互和控件参考。
- 确认第一阶段是手工模式,不强依赖订单和任务。
### CP2后端手工生成接口
- 已完成:新增业务接口 `POST /api/reservation/invoices/manual-generations`
- 已完成:新增生成记录表 `workflow_reservation_invoice_generation` 和业务审计。
- 已完成:根据请求 payload 填充受控 Excel 模板 `templates/reservation-invoice/proforma-invoice-v1.xlsx`
- 已完成:复用平台 Excel 转 PDF Adapter 生成 PDF并上传阿里云 OSS。
- 已完成:返回 PDF URL、对象 Key、金额摘要和生成状态。
- 已完成:失败记录保留已上传成功的 Excel / PDF object key业务审计完成后才标记 `SUCCEEDED`
- 第一版限制:只支持 `source_type=MANUAL``template_code=PROFORMA_INVOICE_V1`、最多 10 条费用明细;暂不提供历史列表、详情查询、任务 / 订单预填和客户联系人目录查询接口。
### CP3前端 Manual Invoice 页面
- 基于当前 `invoice.html` 原型改造成前端项目内页面。
- 字段控件按“输入 / 选择 + 可手填 / 只读计算 / 模板配置”实现。
- 提交到后端业务接口,不调用调试上传接口。
- 展示 PDF 生成结果、错误提示和下载 / 预览入口。
### CP4任务 / 订单预填
- 从任务详情进入时,使用任务详情 `fields[]`、草稿或确认 payload 预填。
- 从订单详情进入时,要求用户选择具体任务或确认仅使用订单摘要。
- 保持用户可以修改字段后再生成 PDF。
### CP5配置化和历史查询
- 客户 / 联系人目录、房型、Rate、Extra Bed、银行账户和模板版本逐步配置化。
- 增加 Invoice 生成历史列表和详情。
- 明确 OSS 生命周期、重新生成、作废和审计策略。
## 11. 当前待确认事项
已确认并落地:
- 第一阶段接口路径采用 `POST /api/reservation/invoices/manual-generations`
- PDF 生成记录第一阶段必须落库。
- 生成后的 Excel 同步上传 OSS 并保存对象 Key便于排查。
- Invoice Date 第一阶段允许前端提交,后端按酒店本地业务日期字段处理。
- 发票模板初始文件使用当前 `Invoice Templete.xlsx` 改造成受控模板资源。
- VAT 第一阶段固定按 7% 含税反算。
仍待后续确认:
- 客户 / 联系人目录是否进入后端配置 / 数据库维护,还是继续由前端临时静态维护。
- 是否需要 Invoice 历史列表、详情、重新生成和作废接口。
- 是否需要支持超过 10 条费用明细、复制模板行样式和分页。
- 任务 / 订单预填时字段优先级和冲突提示规则。