# ES 海牙文档异步生成接口文档（es_hague_api）

---

## 1. 概述

### 1.1 设计目标

提供一个**异步受理**接口（与 `generate_api.php` 同一异步链路）：调用方传入海牙合并 PDF 生成所需的全部业务参数与文件 URL，接口完成参数校验、信用报告下载、创建异步任务后**立即返回受理结果**（`ProcessMode='async'`，`data=null`），文件由 `task/queue_processor.php` 后台生成，生成完成后写入任务记录 `AsyncVATTasks.ResultData`（`file1/cos_key/cos_url`，`TaskStatus=2`）。

> **030 文件与结果通知**：030 文件由 RPA 程序单独生成；RPA 完成后**调用对方的结果通知接口**（通知 URL 由 RPA 程序固定配置，不在本仓库范围内），任务记录中的 `ResultData.file1/cos_url` + `bizParam` 供 RPA 读取后拼接通知内容。本仓库**不提供**结果回调/查询接口（曾有的 `es_hague_result_api.php` 已移除）。

同时，接口把**请求参数写入 `AsyncVATTasks` 任务表**（RPA 库），通过新增的 **`DataSource`** 字段区分记录来源：

| DataSource | 来源 | 说明 |
| ---------- | ---- | ---- |
| `source`   | `generate_api`（原流程） | SaaS 先把业务数据写入 source 业务表（VATRegInfo 等），generate_api 按 id 读取后存入任务表（默认值，历史数据兼容） |
| `api`      | `es_hague_api`（本接口） | 参数直接由请求传入，由本接口存入任务表 |

> 本接口**不写 source 业务表**（VATRegInfo / VATBusinessRecord / Base_Customer_Company 等），对原 generate_api 流程零影响。

### 1.2 设计要点

- **复用现有生成管线**：`VATDocumentGenerator::generateFromData($dbData, $creditReportPath)` 与数据来源解耦，本接口受理时用请求参数构造等价 `$dbData`（`_source='api'`）存入任务记录；队列处理器按 `DataSource='api'` 分流，从 `TaskData.request_data` 重建 `$dbData` 后喂入，OCR/翻译/LibreOffice/合并/上传逻辑完全复用。
- **文件来源**：请求中文件字段值为 `[{fileUrl, fileName}]`（`fileUrl` 为 OSS 相对路径或完整 URL），接口自行解析并下载。
- **安全（SSRF 防护）**：文件 `fileUrl` 为完整 URL 时，域名必须在白名单 `allowed_download_hosts`（config 配置，默认 `usaeu-1259285998.cos.ap-guangzhou.myqcloud.com` / `file.usaeu.com` / `testfile.usaeu.com`，可用环境变量 `ALLOWED_DOWNLOAD_HOSTS` 覆盖）；直接 IP 且为私网/环回/链路本地地址一律拒绝；下载后校验重定向最终域名。`fileName` 一律 `basename()` 净化（防路径穿越）。
- **受理耗时**：仅含参数校验 + 信用报告下载 + 建任务，秒级返回；接口超时上限 2 分钟（`set_time_limit(120)`）。文件生成耗时在队列侧（不占用接口连接）。

### 1.3 核心架构

```
SaaS / 统一中转接口(app_withdrawn/api.php)
   │ POST { PushType, Country, bizParam, Data }
   ▼
es_hague_api.php（异步受理）
   ├── 参数校验（PushType/Country/bizParam/Data）
   ├── buildBusinessDataFromApiParams() 构造 $dbData（字段映射、AR校验、必填校验、HK判定）
   ├── 下载信用报告（非香港公司）
   ├── createApiTask() → AsyncVATTasks(DataSource='api', TaskStatus=0 pending)
   ▼
{ code:200, msg:success, ProcessMode:'async', data:null, bizParam }

task/queue_processor.php（后台，持续运行）
   ├── 捞取 pending 任务
   ├── DataSource='api' → 从 TaskData.request_data 重建 $dbData
   ├── generateFromData() → 生成海牙合并PDF → 上传COS
   ├── completeApiTask() → TaskStatus=2，ResultData={file1, cos_key, cos_url, process_mode:'async'}
   └── 失败达重试上限 → failApiTask()（TaskStatus=3）→ AsyncResultNotifier 按 ES_HAGUE_RESULT_CALLBACK_URL 回调调用方（未配置不回调）
   ▼
RPA 程序（外部）
   ├── 读取任务记录 → 生成 030 文件
   └── 调用对方结果通知接口（通知 URL 由 RPA 固定配置），内容含海牙文件 URL + 030 URL
```

---

## 2. 接口规格

### 2.1 基本信息

| 项目 | 值 |
| ---- | ---- |
| **接口名称** | ES 海牙文档异步生成接口 |
| **请求方式** | POST |
| **Content-Type** | `application/json` |
| **入口文件** | `public/es_hague_api.php` |
| **本地测试** | `http://127.0.0.1:8080/es_hague_api.php`（`php -S 0.0.0.0:8080 -t public`） |
| **生产部署** | 与 `generate_api.php` 相同站点目录，`https://<host>/es_hague_api.php` |
| **超时** | 2 分钟（120 秒，受理阶段；文件生成在队列侧） |

### 2.2 请求参数

| 参数 | 类型 | 必填 | 说明 |
| ---- | ---- | ---- | ---- |
| `PushType` | string | 是 | 业务类型，仅支持 `ES_VAT_REGISTER_HAGUE` / `ES_EPR_REGISTER_HAGUE` / `ES_EPR_REGISTER_030`（当前均走海牙合并 PDF 生成） |
| `Country` | string | 是 | 国家二字码，固定 `ES` |
| `bizParam` | object | 是 | 业务标识信息。**原样回传到响应**。`BusinessSerialNumber` 为业务流水号；`BusinessId`（兼容拼写 `Businessld`）为业务 ID，**必填**，写入任务表 `VATBusinessRecordId` |
| `Data` | object | 是 | 业务数据（扁平业务字段，见 [3. Data 业务字段结构](#3-data-业务字段结构)） |

**请求体示例：**

```json
{
  "PushType": "ES_VAT_REGISTER_HAGUE",
  "Country": "ES",
  "bizParam": {
    "BusinessSerialNumber": "EVAT20260811001",
    "BusinessId": 123456
  },
  "Data": {
    "NameCN": "枝江市霞陆逊商贸有限公司",
    "NameEng": "zhijiangshixialuxunshangmaoyouxiangongsi",
    "RegNumber": "91420583MADDMTJE7M",
    "LegalPersonName": "刘振华",
    "LegalPersonFullNamePinYin": "ZHENHUA LIU",
    "LegalPersonIdNumber": "510681198706252865",
    "LegalPersonCountry": "中国",
    "LegalPersonBirthDate": "1987-06-25",
    "LegalPersonIdStartDate": "2019-04-15",
    "LegalPersonIdEndDate": "2039-04-15",
    "CompanyCountry": "中国",
    "CompanyAddressLine1En": "No. W82, 2nd floor, ...",
    "CityEngName": "Zhi Jiang",
    "CompanyAddressPostcode": "443200",
    "RegisteredCapital": "100000.00",
    "EstablishmentDate": "2024-03-22",
    "AR": "M",
    "SpecsName": "海牙",
    "BusinessLicensePic": "[{\"fileUrl\":\"common-test/2026/08/06/xxx.jpg\",\"fileName\":\"license.jpg\"}]",
    "LegalPersonIdNumberFrontPic": "[{\"fileUrl\":\"common-test/2026/08/06/yyy.jpg\",\"fileName\":\"idfront.jpg\"}]",
    "LegalPersonIdNumberBackPic": "[{\"fileUrl\":\"common-test/2026/08/06/zzz.jpg\",\"fileName\":\"idback.jpg\"}]",
    "CreditReportPic": "[{\"fileUrl\":\"common-test/2026/08/06/credit.pdf\",\"fileName\":\"credit_report.pdf\"}]"
  }
}
```

### 2.3 响应格式

**成功响应（异步受理，文件生成完成后另行通知）：**

```json
{
  "code": 200,
  "msg": "success",
  "ProcessMode": "async",
  "data": null,
  "bizParam": {
    "BusinessSerialNumber": "EVAT20260811001",
    "BusinessId": 123456
  }
}
```

> - `ProcessMode` 恒为 `async`，`data` 恒为 `null`（本接口只负责受理，不返回文件）。
> - `bizParam` 为请求**原样回传**。
> - 受理成功即表示任务已创建（`AsyncVATTasks`：`DataSource='api'`，`TaskStatus=0` pending，请求参数入 `TaskData`，非香港公司的信用报告本地路径入 `CreditReportFilePath`），等待 `queue_processor` 后台生成。
> - 生成结果（海牙合并 PDF）由队列处理完成后写入任务记录：`TaskStatus=2`，`ResultData` 含 `file1/cos_key/cos_url/process_mode:'async'`。
> - 030 文件由 RPA 程序单独生成，RPA 完成后调用对方的结果通知接口（URL 由 RPA 固定配置）。

**失败响应（业务校验失败 400）：**

```json
{
  "code": 400,
  "msg": "AR字段缺失或无效：X，仅允许M(原模板)或O(Onesea模板)",
  "ProcessMode": "async",
  "data": null,
  "bizParam": {
    "BusinessSerialNumber": "EVAT20260811001",
    "BusinessId": 123456
  }
}
```

**失败响应（服务器错误 500）：**

```json
{
  "code": 500,
  "msg": "服务器内部错误，请稍后重试",
  "ProcessMode": "async",
  "data": null,
  "bizParam": null
}
```

> 500 对外仅返回通用信息，内部异常详情只写服务端日志（`logs/es_hague_api_YYYYMMDD.log`），避免泄露数据库连接信息、文件系统路径等内部细节。

### 2.4 状态码说明

| code | 含义 | 示例 |
| ---- | ---- | ---- |
| 200 | 请求成功，任务已受理（异步生成） | `data=null`，`ProcessMode='async'` |
| 400 | 参数错误 / 业务校验失败 / 信用报告不可下载 | 缺少参数 / AR 无效 / 必填字段缺失 / fileUrl 404 |
| 405 | 请求方法不允许 | 非 POST |
| 500 | 服务器内部错误 | 数据库连接失败 / 建任务异常 |

---

## 3. Data 业务字段结构

`Data` 为**扁平业务字段对象**：键为业务字段名，值为字段值。文件类字段的值为 JSON 数组字符串，含 `fileUrl`（OSS 相对路径或完整 URL）+ `fileName`；数组字段（如 `RegisteredCapitalCurrency`）以逗号连接后使用。

### 3.1 业务字段清单

| 字段 | 类型 | 必填 | 说明 |
| ---- | ---- | ---- | ---- |
| `NameCN` | 字符串 | 是 | 企业中文名称 |
| `NameEng` | 字符串 | 是 | 企业英文全称（不能含中文） |
| `RegNumber` | 字符串 | 是 | 营业执照号码 |
| `LegalPersonName` | 字符串 | 是 | 法人中文姓名 |
| `LegalPersonFullNamePinYin` | 字符串 | 是 | 法人英文姓名（拼音，不能含中文） |
| `LegalPersonIdNumber` | 字符串 | 是 | 法人证件号码 |
| `LegalPersonCityEngName` | 字符串 | 是 | 法人所在城市（英文） |
| `LegalPersonIdStartDate` | 日期 | 是 | 证件有效期开始时间（YYYY-MM-DD） |
| `LegalPersonIdEndDate` | 日期 | 是 | 证件有效期截止时间（YYYY-MM-DD） |
| `LegalPersonIDCardType` | 字符串 | 是 | 法人证件类型：`1`/`身份证`/`IDCard` → `IDCard`；`2`/`护照`/`Passport` → `Passport`（受理时规范化） |
| `LegalPersonCountry` | 字符串 | 是 | 法人国籍（**中文原值**，如 `中国`；须在国家数据中存在；任务表 AsyncVATTasks 国家列存英文名 `LegalPersonCountry_en`） |
| `LegalPersonCountry_en` | 字符串 | 是 | 法人国籍英文名（如 `China`）—— **以接口传值为准** |
| `LegalPersonBirthDate` | 日期 | 是 | 法人出生日期（YYYY-MM-DD） |
| `LegalPersonAddressProvinceEn` | 字符串 | 是 | 法人地址省份（英文） |
| `CompanyAddressLine1En` | 字符串 | 是 | 公司地址第一行（英文，不能含中文） |
| `CompanyAddressLine2En` | 字符串 | 否 | 公司地址第二行（英文） |
| `CompanyAddressPostcode` | 字符串 | 是 | 公司地址邮编 |
| `CompanyAddressProvinceEn` | 字符串 | 是 | 公司地址省份（英文） |
| `RegAddressEng` | 字符串 | 是 | 注册地址（英文，不能含中文） |
| `CityEngName` | 字符串 | 是 | 公司所在城市（英文） |
| `CompanyCountry` | 字符串 | 是 | 公司所在国家（**中文原值**，如 `中国`/`香港`；`香港`/`HK`/`HONG KONG` 判定为香港公司；任务表 AsyncVATTasks 国家列存英文名 `CompanyCountry_en`） |
| `CompanyCountry_en` | 字符串 | 是 | 公司国家英文名（如 `China`）—— **以接口传值为准** |
| `CompanyCountryCode` | 字符串 | 是 | 公司国家区域代码（如 `156`）—— **以接口传值为准** |
| `Country` | 字符串 | 是 | 兼容字段，与 `CompanyCountry` 同义（取其一） |
| `RegisteredCapital` | 字符串 | 否 | 注册资本 |
| `RegisteredCapitalCurrency` | 数组/字符串 | 否 | 注资币种（数组以逗号连接） |
| `EstablishmentDate` | 日期 | 否 | 成立日期（YYYY-MM-DD） |
| `AR` | 字符串 | 是 | 模板选择：`M`=原模板，`O`=Onesea模板 |
| `SpecsName` | 字符串 | 否 | 产品规格名称；含 `免海牙` 时走免海牙流程（生成 `Documento_Empresarial`） |
| `HagueType` | 整数 | 否 | 香港公司类型：1=包装法，2=包装法+VAT，3=VAT（仅香港公司使用） |
| `LegalPersonGender` | 字符串 | 否 | 性别（`1`/`男` 或 `2`/`女`，亦兼容 `H`/`HOMBRE`/`M`/`MUJER`）；任务表 AsyncVATTasks 存 `H`(男)/`M`(女)，受理时即规范化 |
| `Nationality` | 字符串 | 否 | 民族（`1`=汉 HAN，`2`=回 HUI），身份证翻译件使用 |
| `LegalPersonEmail` | 字符串 | 否 | 法人邮箱 |
| `LegalPersonPhone` | 字符串 | 否 | 法人手机号码 |
| `LegalPersonIDCardAddress` | 字符串 | 否 | 证件地址（中文原值，与 `LegalPersonIDCardAddressEng` 二选一） |
| `LegalPersonIDCardAddressEng` | 字符串 | 否 | 证件地址（拼音/英文） |
| `BusinessCode` | 字符串 | 否 | 业务编码（用于文件名） |
| `Code` | 字符串 | 否 | 编码 |
| `BusinessLicenseDirection` | 数字 | 否 | 营业执照方向：`1`=横版范围、`2`=横版提示、`3`=竖版范围、`4`=竖版提示；不传则自动检测（OCR 判定） |

> **字段取值规则**（ES 海牙）：
> - `LegalPersonCountry` / `CompanyCountry` 传**中文原值**（如 `中国`，用于香港公司判定）；国家英文名与区域代码**以接口传值为准**（`LegalPersonCountry_en` / `CompanyCountry_en` / `CompanyCountryCode` 必填，调用方传入）；**任务表 AsyncVATTasks 的国家列存英文名**（如 `China`），中文原值不入任务表国家列
> - `LegalPersonIDCardType` 统一规范化为 `IDCard` / `Passport`
> - `LegalPersonGender` 任务表 AsyncVATTasks 存 `H`(男) / `M`(女)，受理时由传入值（`1`/`男`/`H`/`HOMBRE` → `H`；`2`/`女`/`M`/`MUJER` → `M`）规范化

### 3.2 文件类字段清单

文件字段值为 JSON 数组字符串：`"[{\"fileUrl\":\"<OSS相对路径或完整URL>\",\"fileName\":\"<文件名>\"}]"`。`fileUrl` 为相对路径时自动拼接 `source_oss_base_url`（config 配置，默认 `https://usaeu-1259285998.cos.ap-guangzhou.myqcloud.com`）。

| 业务字段名 | 必填 | 用途 |
| ---------- | ---- | ---- |
| `BusinessLicensePic` | 是 | 非香港公司=工商营业执照；香港公司=BR 文件（商业登记证） |
| `LegalPersonIdNumberFrontPic` | 是 | 证件正面照（所有公司） |
| `LegalPersonIdNumberBackPic` | 是 | 证件反面照（所有公司） |
| `CreditReportPic` | 非香港公司必填 | 企业信用报告（PDF/ZIP/RAR），**受理阶段下载** |
| `LegalSignedFile` | 免海牙必填 | 法人签名文件（免海牙流程使用） |
| `BusinessCRFile` | 香港公司必填 | CR 文件（注册证书） |
| `BRFooterPic` | 香港公司必填 | BR 脚码图片 |
| `CRSignerPic` | 香港公司必填 | CR 签发人图片 |
| `CompanyParticularsPic` | 香港公司 HagueType 1/2 必填 | 查册文件（Company Particulars） |

### 3.3 bizParam 字段说明

| 字段 | 类型 | 说明 |
| ---- | ---- | ---- |
| `BusinessSerialNumber` | 字符串 | 业务流水号（写入任务记录，便于追溯） |
| `BusinessId` | 数字 | 业务 ID（兼容拼写 `Businessld`），写入任务表 `VATBusinessRecordId` |

> `bizParam` 中其他字段任意，均**原样回传**到响应。

---

## 4. 生成与存储流程

```
请求
 ├─ 校验 PushType/Country/bizParam/Data
 ├─ buildBusinessDataFromApiParams(Data, bizParam)
 │    ├─ 字段映射 + AR校验(M/O) + 必填校验 + 国家校验（复用 generate_api 校验逻辑）
 │    ├─ 香港公司判定（CompanyCountry 含 香港/HK/HONG KONG）
 │    └─ 输出 $dbData（_source='api'，结构等价 getVATBusinessData）
 ├─ 非香港公司：下载 CreditReportPic → 信用报告本地路径
 ├─ createApiTask() → AsyncVATTasks 插入记录（DataSource='api'，TaskStatus=0 pending，请求参数入 TaskData）
 └─ 返回 { code:200, ProcessMode:'async', data:null, bizParam }（立即，不等待生成）

queue_processor（后台异步阶段）
 ├─ 捞取 pending 任务（DataSource='api' 与 source 任务统一排队）
 ├─ DataSource='api' 分支：从 TaskData.request_data 重建 $dbData（复用 buildBusinessDataFromApiParams）
 ├─ generateFromData($dbData, CreditReportFilePath)
 │    ├─ 香港公司：CR/BR/查册/身份证 → 4份授权书 + 翻译件
 │    ├─ 非香港公司：海牙=4份授权书+信用报告+营业执照+身份证翻译件；免海牙=免海牙授权书+...
 │    ├─ Word → PDF（LibreOffice）→ 合并
 │    └─ 上传 COS → cos_url + cos_key
 ├─ completeApiTask() → TaskStatus=2 完成，ResultData={file1, cos_key, cos_url, process_mode:'async'}
 └─ 失败：可重试则置回 pending，否则 failApiTask()（TaskStatus=3 + ErrorMessage）

RPA 程序（外部）
 └─ 读取任务记录（ResultData.file1/cos_url + bizParam）→ 生成 030 文件
    → 调用对方结果通知接口（通知 URL 由 RPA 固定配置），通知内容含海牙文件 URL + 030 URL
```

**失败路径（受理阶段）**：任一步校验/下载失败 → 返回 400（参数/文件问题）或 500（服务器错误），**不创建任务**。

**失败路径（生成阶段）**：队列侧生成失败 → 任务置回 pending 重试（`RetryCount`/`MaxRetries`），达到上限后 `failApiTask()`（TaskStatus=3 + ErrorMessage），并调用 `AsyncResultNotifier` 按 `ES_HAGUE_RESULT_CALLBACK_URL`（.env，可选）向调用方 POST 失败回调——信封 `{code:500, msg:'failed', ProcessMode:'async', data:{task_id, status:'failed', error}, bizParam}`；未配置则不回调，重试期间不通知（避免重复打扰）。RPA 如需感知失败，可读取任务记录状态。

**任务表字段说明（AsyncVATTasks）：**

| 字段 | 本接口写入值 |
| ---- | ---- |
| `DataSource` | `api`（generate_api 流程为 `source`） |
| `VATBusinessRecordId` | `bizParam.BusinessId` |
| `TaskStatus` | 0 待处理 → 1 处理中 → 2 完成 / 3 失败（队列处理器维护） |
| `TaskData` | 完整请求参数 JSON（`request_data` + `bizParam` + 业务字段） |
| `ResultData` | `{"file1":"<OSS相对路径>","cos_key":"...","cos_url":"...","process_mode":"async"}`（生成完成后写入） |
| `CreditReportFilePath` | 非香港公司为信用报告本地路径 |

---

## 5. 测试

### 5.1 单元/结构测试（无需外部依赖）

```bash
php scripts/test_es_hague_api_params.php          # 参数构造器：URL解析/字段映射/校验/香港判定
php scripts/test_generate_from_data_compat.php    # 重构结构回归：方法存在性/签名/迁移文件
php scripts/test_datasource_migration.php         # DataSource 迁移检查（需连接RPA库）
```

### 5.2 接口契约测试（需本地服务）

```bash
php -S 0.0.0.0:8080 -t public &
php scripts/test_es_hague_api_endpoint.php        # 参数校验/异步受理响应格式/回传 契约断言
```

### 5.3 cURL 示例

```bash
curl -X POST http://127.0.0.1:8080/es_hague_api.php \
  -H "Content-Type: application/json" \
  -d '{
    "PushType": "ES_VAT_REGISTER_HAGUE",
    "Country": "ES",
    "bizParam": { "BusinessSerialNumber": "EVAT20260811001", "BusinessId": 123456 },
    "Data": {
      "NameCN": "枝江市霞陆逊商贸有限公司",
      "NameEng": "zhijiangshixialuxunshangmaoyouxiangongsi",
      "RegNumber": "91420583MADDMTJE7M",
      "LegalPersonFullNamePinYin": "ZHENHUA LIU",
      "LegalPersonName": "刘振华",
      "LegalPersonIdNumber": "510681198706252865",
      "LegalPersonCityEngName": "Guang Han",
      "LegalPersonIdStartDate": "2019-04-15",
      "LegalPersonIdEndDate": "2039-04-15",
      "LegalPersonIDCardType": "1",
      "LegalPersonCountry": "中国",
      "LegalPersonBirthDate": "1987-06-25",
      "LegalPersonAddressProvinceEn": "Sichuan",
      "CompanyAddressLine1En": "No.1 Test Road",
      "CompanyAddressPostcode": "443200",
      "CompanyAddressProvinceEn": "Hubei",
      "RegAddressEng": "No.1 Test Road",
      "CityEngName": "Zhi Jiang",
      "CompanyCountry": "中国",
      "AR": "M",
      "SpecsName": "海牙",
      "BusinessLicensePic": "[{\"fileUrl\":\"common-test/2026/08/06/license.jpg\",\"fileName\":\"license.jpg\"}]",
      "LegalPersonIdNumberFrontPic": "[{\"fileUrl\":\"common-test/2026/08/06/idf.jpg\",\"fileName\":\"idf.jpg\"}]",
      "LegalPersonIdNumberBackPic": "[{\"fileUrl\":\"common-test/2026/08/06/idb.jpg\",\"fileName\":\"idb.jpg\"}]",
      "CreditReportPic": "[{\"fileUrl\":\"common-test/2026/08/06/credit.pdf\",\"fileName\":\"credit_report.pdf\"}]"
    }
  }'
```

响应（受理成功，秒级返回）：

```json
{
  "code": 200,
  "msg": "success",
  "ProcessMode": "async",
  "data": null,
  "bizParam": {
    "BusinessSerialNumber": "EVAT20260811001",
    "BusinessId": 123456
  }
}
```

### 5.4 Python 示例

```python
import requests

url = "http://127.0.0.1:8080/es_hague_api.php"
payload = {
    "PushType": "ES_VAT_REGISTER_HAGUE",
    "Country": "ES",
    "bizParam": {"BusinessSerialNumber": "EVAT20260811001", "BusinessId": 123456},
    "Data": {
        "NameCN": "枝江市霞陆逊商贸有限公司",
        "NameEng": "zhijiangshixialuxunshangmaoyouxiangongsi",
        # ... 其余业务字段 ...
        "BusinessLicensePic": '[{"fileUrl":"common-test/2026/08/06/license.jpg","fileName":"license.jpg"}]',
        "CreditReportPic": '[{"fileUrl":"common-test/2026/08/06/credit.pdf","fileName":"credit_report.pdf"}]',
    },
}
resp = requests.post(url, json=payload, timeout=30)
result = resp.json()
if result["code"] == 200 and result["ProcessMode"] == "async":
    print("受理成功，文件后台生成中")   # data 为 null，文件结果由 RPA 完成后通知
else:
    print("失败:", result["msg"])
```

---

## 6. 运维与扩展

| 事项 | 说明 |
| ---- | ---- |
| 数据库迁移 | 需先执行 `database/add_datasource_to_async_tasks.sql`（或 `php scripts/run_datasource_migration.php`）为 `AsyncVATTasks` 增加 `DataSource` 字段。**未迁移不影响旧流程**：`createTask` 会自动探测列存在性，缺失时跳过该列写入；`checkExistingAsyncTask` 也会在列缺失时忽略 DataSource 过滤。全新环境基表脚本 `create_async_task_table.sql` 已含该列 |
| `source_oss_base_url` | config 配置项，文件字段为相对路径时拼接的 OSS 域名（可用环境变量 `SOURCE_OSS_BASE_URL` 覆盖） |
| `allowed_download_hosts` | config 配置项，SSRF 防护白名单（逗号分隔，可用环境变量 `ALLOWED_DOWNLOAD_HOSTS` 覆盖）；默认含 usaeu COS 域名 + `file.usaeu.com` + `testfile.usaeu.com`。若接入方文件在其他域名，须在此加入 |
| 临时文件 | 受理阶段下载的信用报告（`temp/api_credit_*`）**在请求退出时不清理**（需存活到队列处理），由异步链路清理：`generateFromData()` 注册的临时文件清理 + `queue_processor` 启动时清理超 20 分钟旧目录 |
| 日志 | `logs/es_hague_api_YYYYMMDD.log`（受理侧）、`logs/queue_processor_YYYYMMDD.log`（生成侧，Logger 已支持自定义日志类型，generate_api/queue_processor 行为不变） |
| 任务记录 | 本接口任务写入 `AsyncVATTasks`，`DataSource='api'`；`generate_api` 的 `checkExistingAsyncTask` 只检查 `source` 任务，API 任务不会阻塞旧流程 |
| 队列依赖 | 生成依赖 `task/queue_processor.php` 持续运行；API 受理成功即视为提交成功，文件生成结果由队列侧写入任务记录。**queue_processor 所在环境必须同步部署含 `processApiTask` 分支的新代码**（旧代码会把 API 任务按 source 流程处理导致失败） |
| 任务分流 | `VATAsyncProcessor` 按 `DataSource='api'` 识别 API 任务；DataSource 列未迁移（列缺失）时自动用 `TaskData.created_by='es_hague_api'` / `request_data` 标记兜底判断，source 任务不受影响。建议仍执行迁移以获得完整语义 |
| 结果通知 | 由 RPA 程序在生成 030 后调用对方的结果通知接口（URL 由 RPA 固定配置） |
| 最终失败回调 | 生成阶段任务达到最大重试次数仍失败时，`AsyncResultNotifier` 按 `ES_HAGUE_RESULT_CALLBACK_URL`（.env，可选）向调用方 POST 错误信息（信封 `{code:500, msg:'failed', ProcessMode:'async', data:{task_id,status:'failed',error}, bizParam}`）；未配置不回调，重试期间不通知。超时秒数 `ES_HAGUE_CALLBACK_TIMEOUT`（默认 10） |
| 重复请求拦截（幂等） | 已实现：受理时按 `bizParam.BusinessSerialNumber`（查 API 来源 0/1 任务）+ `bizParam.BusinessId`（查任意来源 0/1 任务）**双向判重**，命中返回 400（已完成/失败的任务允许重新提交） |
| Token 认证 / 限流 | 后续扩展：增加 `Authorization: Bearer <token>` 校验、接口限流 |

---

## 7. 030 文件与结果通知（RPA 侧）

030 文件由 **RPA 程序单独生成**，不在本接口与队列处理范围内。整体协作如下：

1. **本仓库（受理+生成）**：`es_hague_api` 受理后，`queue_processor` 生成海牙合并 PDF，结果写入任务记录 `ResultData`（`file1/cos_key/cos_url`，`TaskStatus=2`）。
2. **RPA 程序（030 + 通知）**：
   - 读取任务记录（按 `bizParam.BusinessId` / `VATBusinessRecordId` 定位），获取海牙文件结果；
   - 生成 030 文件并上传 OSS；
   - 调用**对方的结果通知接口**，通知内容含海牙文件 URL + 030 文件 URL + 业务标识（`bizParam` 原样存于 `TaskData`）。
3. **通知地址**：对方结果通知接口的 URL 由 RPA 程序固定配置，不经过本仓库。

> 本仓库不再提供 HTTP 结果回调接口（`es_hague_result_api.php` 已移除）。RPA 如需在任务记录中保留 030 状态（便于排查），可复用 `AsyncTaskManager` 提供的方法：
>
> | 方法 | 作用 |
> | ---- | ---- |
> | `completePdf030($taskId, $pdf030Url, $extra)` | 记录 030 成功：`pdf030_result=2`、`pdf030_result_url`，并 merge 进 `ResultData` |
> | `markPdf030Processing($taskId)` | 标记 030 生成中（可选，`pdf030_result=1`） |
> | `failPdf030($taskId, $errorMsg)` | 标记 030 失败（`pdf030_result=3` + ErrorMessage） |
>
> `pdf030_result` / `pdf030_result_url` 列由 `add_vat_fields_to_async_tasks.sql` 迁移提供；上述方法会探测列存在性，缺失时降级仅写 `ResultData` JSON，不报错。
