# 03 - NL（荷兰）文档生成说明

> 创建日期：2026-09-23
> 状态：进行中
> 范围：荷兰 VAT 申报文档自动生成链路（定时 job + API 触发）

## 一、概述

荷兰（NL）链路面向 **VAT 申报文档自动化**：定时 job 轮询源库待推送记录，按国家模板目录下多个 Word 模板逐份渲染（占位符替换 + 证件照片替换 + 身份证 OCR 补字段），各模板转 PDF 后**合并成一份主文件 PDF**，执照（如模板名含「执照」）单独生成；主文件与执照分别上传腾讯云 COS 并回写附件表，最后回写推送状态、写生成过程日志。

触发方式有两种：

- **定时 job**：`python -m app.jobs.generate_documents_job`（每分钟轮询，空结果休眠 10 分钟；`--once` 跑一轮退出）。
- **API**：`POST /api/enterprise/{code}` → `generate_enterprise_documents(code)`。

## 二、处理流程

```
调度查询(get_schedule_recode) ── 取附件(get_annex_files)
        │
        ▼
generate_nl_documents(record, files)
  1. 确定国家（Country = 'NL'）
  2. 查找模板 find_templates('nl')
  3. _build_values      字段映射（公司/法人/证件/地址…）
  4. _prepare_images    下载附件(营业执照/身份证) → 转图片
  5. _ocr_idcard_fields 身份证 OCR → 补「签发城市 / 法人性别」
  6. render_templates   逐模板渲染：docx → PDF（含证件照替换）
  7. merge_pdfs         合并所有 PDF → 主文件 main.pdf
  8. upload_file        主文件上传 COS；执照 PDF 单独上传 COS
  9. _write_back_status 回写 VATRegInfo.PushTaxBureauStatus（成功 6 / 失败 -1）
 10. _write_back_annex  回写 Base_AnnexesFile（main / license 分类）
 11. _flush             写生成日志 vat_nl_file_log（单行合并）
```

关键点：

- 单附件/单模板失败**跳过继续**，记日志，不中断整条。
- 回写失败仅记日志，不阻断主流程。
- 生成日志为**每 code 一行**（见「日志表」）。

## 三、数据表

### 源库（vat_db，只读）

| 表 | 用途 |
| --- | --- |
| `VATBusinessRecord` | VAT 业务登记记录（`Country`、`Code`） |
| `VATRegInfo` | VAT 注册信息（回写列 `PushTaxBureauStatus`） |
| `Base_Customer_Company` | 企业信息（公司中/英文名、地址、邮编、国家、法人等） |
| `Base_Sales_Platform_Shop` | 店铺信息（`SellerID`） |
| `Base_AnnexesFile` | 附件文件（营业执照 / 身份证正反面；生成文件回写目标） |

调度 SQL 见 `app/repositories/nl_enterprise.py::_build_detail_sql`，当前测试条件硬编码：

```sql
br.Country='nl' AND ri.PushType='101' AND br.Code = 'POEORI20260907000044'
```

### 目标库（rpa，本系统所有）

- `render_record`：文件替换结果记录（由 `init_db` 建表/灌入，供 `/api/render/records` 查询）。
- `vat_nl_file_log`：生成过程日志（见「日志表」）。

## 四、模板与占位符

- 模板目录：`code/templates/nl/*.docx`。
- 占位符形式（`app/services/placeholder.py`）：`<<key>>`、`{{key}}`、`((key))`、`《key》`、`【key】`，键支持中文/字母/数字/下划线/点/短横线。
- 字段值由 `_build_values`（`app/services/enterprise_document.py`）从源库字段映射到占位符 key。
- 模板名含「执照」的，会被识别为执照文件单独处理。

## 五、输出与 COS

- 主文件：`{gen_file_prefix}{企业名}{gen_file_suffix}.pdf`（合并后）。
- 执照：执照模板渲染出的 PDF 单独上传。
- 上传：`app/services/cos_storage.py::upload_file`，key 为 `{COS_BASE_DIR}/{文件名}`，按扩展名设置 `Content-Type`，并带 `Content-Disposition: attachment; filename="..."`；返回公开 URL。
- 回写：`Base_AnnexesFile`，`NL_FILE_CATEGORIES` 中 `main` / `license` 对应不同 `file_category_id`。

## 六、日志表（vat_nl_file_log）

- 写入函数：`app/repositories/vat_generate_log.py::insert_vat_generate_log`。
- 结构（`id / batch_id / code / vat_reg_info_id / enterprise_name / country / step / level / message / detail / created_at / updated_at`）。
- **语义（重要）**：每个 `code` 只保留**一行**，`step` 置空（NULL）：
  - `message`：所有步骤信息按行合并（每步一行 `[level] step: message`，换行连接）。
  - `detail`：所有异常信息（traceback）空行合并。
  - `level`：本次取到的最高级别（`error` > `warning` > `info`）。
  - 每次生成对该行覆盖更新，`updated_at` 始终是「最近一次整体更新」的单一时间，不再出现多行时间不一致。

## 七、代码结构

| 文件 | 职责 |
| --- | --- |
| `app/jobs/generate_documents_job.py` | 轮询 job 入口（`run` / `run_once`） |
| `app/services/nl_document.py` | 编排 `generate_nl_documents`、`generate_enterprise_documents` |
| `app/repositories/nl_enterprise.py` | NL 源库 SQL 查询 / 状态与附件回写 / `NL_FILE_CATEGORIES` |
| `app/repositories/vat_generate_log.py` | 写日志 `vat_nl_file_log` |
| `app/services/enterprise_document.py`（共享） | 字段映射 / 图片下载转图 / OCR / 模板渲染 / 回写 helper |
| `app/services/cos_storage.py`（共享） | COS 上传 |
| `app/services/converter.py`（共享） | docx/xlsx → PDF（COM / LibreOffice） |
| `app/services/pdf_merge.py`（共享） | PDF 合并（pymupdf） |
| `app/core/countries.py`（共享） | 国家代码映射 + 模板查找 |

## 八、配置项（.env）

| 变量 | 说明 |
| --- | --- |
| `SOURCE_DB_URL` | 源库（SQL Server / MySQL / PostgreSQL），留空用本地 SQLite 兜底 |
| `TARGET_DB_URL` | 目标库（rpa），同上 |
| `TENCENT_SECRET_ID` / `TENCENT_SECRET_KEY` / `TENCENT_REGION` | 腾讯云 OCR（身份证/营业执照） |
| `COS_SECRET_ID` / `COS_SECRET_KEY` / `COS_REGION` / `COS_BUCKET` / `COS_BASE_DIR` | 腾讯云 COS 上传 |
| `PDF_CONVERTER` | `auto`（先 COM 后 LibreOffice）/ `msoffice`（仅 Windows COM）/ `libreoffice`（仅 Linux） |
| `SOFFICE_PATH` | LibreOffice 可执行文件路径（Linux 常用 `soffice`） |
| `JOB_POLL_SECONDS` / `JOB_EMPTY_SLEEP_SECONDS` | job 轮询 / 空结果休眠间隔 |
| `APP_ENV` | 选择配置文件（未设→`.env`；`dev`→`.env.dev`；`prod`→`.env.prod`） |

## 九、部署与使用

### 通用前置

- Python 3.10+（代码使用 `str | None` 等新语法，建议 3.11/3.12）。
- 安装依赖：`pip install -r requirements.txt`（开发另装 `requirements-dev.txt`）。
- 配置：`cp .env.example .env`（Linux）/ `copy .env.example .env`（Windows），按环境填写数据库、OCR、COS。
- 目标库建表：`python -m app.init_db`（本系统表 `render_record` 由 ORM 建；`vat_nl_file_log` 需按 README DDL 手动建，或用 `init_db` 之外的 DDL）。

### Windows

1. 安装 Python 3.10+；安装 **Microsoft Office（Word + Excel）**（PDF 转换走 COM 需要）。
2. 安装 SQL Server 时需 **ODBC Driver 17/18**；`.env` 里 `SOURCE_DB_URL` 用 `mssql+pyodbc://...?driver=ODBC+Driver+18+for+SQL+Server`。
3. 依赖：`pip install -r requirements.txt`（含 `pywin32`）。
4. `PDF_CONVERTER=auto` 或 `msoffice`。
5. 启动 API（PowerShell，在 `code` 目录）：
   ```powershell
   python -m uvicorn app.main:app --host 0.0.0.0 --port 8000
   ```
6. 跑 NL job（另开窗口，测试用 `--once`）：
   ```powershell
   python -m app.jobs.generate_documents_job --once
   ```

#### 注册为 Windows 任务计划程序（开机守护，生产推荐）

`code/win_bat/vat_nl_file_start_job.bat` 是供任务计划程序调用的启动脚本：先 `cd /d "%~dp0.."` 切到 `code\` 目录（win_bat 的上一级，确保能找到 `.env` 与 `templates/`），再 `python -m app.jobs.generate_documents_job` 常驻轮询。

**职责划分（避免冲突）**：任务计划程序只做「进程守护」——开机启动一次、崩溃自动重启，它**不负责**定时频率；业务调度（多久查一次、查什么）完全由 job 内部的 `while True` 轮询负责（间隔 `JOB_POLL_SECONDS` / `JOB_EMPTY_SLEEP_SECONDS`）。二者各管一段、互不冲突。

> ⚠️ 常驻模式（不带 `--once`）下，触发器**不要**设「每 N 分钟重复」，否则每 N 分钟会再拉起一个新实例、多实例叠加重复处理。要由任务计划程序掌握频率，改用 `--once` 一次性模式。

步骤：

1. 启动脚本（已提供，`code/win_bat/vat_nl_file_start_job.bat`）：
   ```bat
   @echo off
   cd /d "%~dp0.."
   python -m app.jobs.generate_documents_job
   ```
2. `Win+R` 输入 `taskschd.msc` 打开「任务计划程序」→ 右侧「创建任务…」（非「创建基本任务」）。
3. 配置任务：
   - 常规：名称如 `vat-doc-job`；勾选「不管用户是否登录都要运行」；「配置」选 Windows 10/11。
   - 触发器：新建「启动时」；可另加「任务失败后：每 1 分钟重启一次，最多 3 次」作崩溃恢复（仅进程异常退出才触发，非周期调度）。
   - 操作：新建「启动程序」→ 程序/脚本填 `E:\...\code\win_bat\vat_nl_file_start_job.bat`，「起始于」填 `E:\...\code\win_bat`。
   - 条件：取消勾选「只有在计算机使用交流电源时才启动此任务」。
4. 环境变量：脚本默认读 `code\.env`（不设 `APP_ENV`）；生产配置取消脚本里 `rem set APP_ENV=prod` 的注释。
5. 若 `python` 不在任务计划程序运行账户的 PATH 中，把脚本里的 `python` 改为 python.exe 绝对路径。
6. 验证：右键任务「运行」，观察 `logs\` 日志持续输出；或先手动 `python -m app.jobs.generate_documents_job --once` 跑一轮冒烟。

### Linux（以 Debian/Ubuntu 为例）

1. 安装 Python 3.10+ 与 venv。
2. 安装 **LibreOffice**（PDF 转换）与 **中文字体**（否则转出的 PDF 中文会变方块）：
   ```bash
   sudo apt-get update
   sudo apt-get install -y libreoffice fonts-noto-cjk
   ```
3. 若用 SQL Server，安装 ODBC：
   ```bash
   sudo apt-get install -y unixodbc unixodbc-dev
   # 再安装 msodbcsql18（微软官方源），并 pip install pyodbc
   ```
   若用 MySQL/PostgreSQL，改用对应 URL（见 `.env.example`）。
4. 依赖：
   ```bash
   python -m venv .venv && source .venv/bin/activate
   pip install -r requirements.txt
   ```
5. `PDF_CONVERTER=libreoffice`，`SOFFICE_PATH=soffice`（已在 PATH）或绝对路径 `/usr/bin/soffice`。
6. 启动 API：
   ```bash
   cd code && python -m uvicorn app.main:app --host 0.0.0.0 --port 8000
   ```
7. 跑 NL job：
   ```bash
   cd code && python -m app.jobs.generate_documents_job --once
   ```
8. 生产环境可用 `systemd` / `supervisord` 守护 API 与 job 两个进程。

## 十、注意事项

- PDF 转换后端与操作系统强相关：**Windows 用 MS Office COM，Linux 用 LibreOffice**；务必匹配 `PDF_CONVERTER`。
- Linux 下务必装中文字体，否则 Word 模板中文渲染异常。
- OCR / COS 需要能访问腾讯云；境内到 `recherche-entreprises.api.gouv.fr`（法国名录，仅 FR 用）在 NL 链路不涉及。
- 调度 SQL 当前含测试硬编码（`br.Code = '...'`），正式上线前需移除。
- 生成日志为「每 code 一行」的合并写入，历史按 step 拆分的旧行不会被新逻辑触碰。
