# 意大利 EPR API 对接文档

设计对齐 `E:\ou\meiou-app\app_withdrawn\docs\UNIFIED_API_DESIGN.md` 中 es_haiya 项目的异步受理模式
（§6 处理模式 / §7 异步结果回调 / §5.2 文件类型枚举）：
受理接口同步校验并快速返回，**两份文件**分别由 PHP（EPR 注册文件）与 RPA（授权书）生成，
RPA 生成完成后调用**统一异步结果通知接口**下发两份文件；结果（**成功和失败**）一律走回调，不回写任何源库状态。

## 1. 受理接口

```
POST /api/epr/italy
Content-Type: application/json; charset=utf-8
Authorization: Bearer <EPR_API_TOKEN>   # 仅当 EPR_API_TOKEN_ENABLED=true 时校验
```

- 仅支持 POST（其他方法返回 405）
- 鉴权：`EPR_API_TOKEN_ENABLED=false`（默认）时跳过 token 验证，直接受理
- 请求体 ≤ `EPR_API_MAX_BODY_BYTES`（默认 2097152 字节 / 2MB）
- 限流：60 次/分/IP

### 1.1 请求体

```json
{
  "PushType": "IT_EPR_REGISTER_FILE",
  "Country": "IT",
  "bizParam": {
    "BusinessId": 789012,
    "BusinessSerialNumber": "IT-2026-0001"
  },
  "Data": { ... }
}
```

| 顶层字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `PushType` | string | 是 | 必须为 `IT_EPR_REGISTER_FILE` |
| `Country` | string | 是 | 大小写不敏感，必须为 `IT` |
| `bizParam` | object | 是 | 业务标识，回调时原样回传 |
| `bizParam.BusinessId` | int/string | 是 | 兼容拼写 `Businessld`（对齐 es_haiya）；落库为 saas_id |
| `bizParam.BusinessSerialNumber` | string | 是 | 流水号；落库为 code，并作为 OSS 唯一目录段 |
| `Data` | object | 是 | 扁平业务字段 |

### 1.2 Data 业务字段

| 字段 | 必填 | 最长 | 说明 |
|---|---|---|---|
| `NameEng` | 是 | 200 | 公司英文名 |
| `RegAddressEng` | 是 | 100 | 公司英文地址 |
| `CompanyAddressPostcode` | 是 | 20 | 邮编 |
| `Country` | 是 | 100 | 原始国家名（如 `中国`），仅存档 |
| `CountryEn` | 是 | 100 | 英文国家名，直接进 DeepL 翻译（API 模式不查询源库） |
| `CountryTwoCode` | 是 | 20 | 国家二字码（如 `CN`，必须为 2 位字母），用于 VAT 解析规则判定 |
| `RegNumber` | 否 | 50 | 营业执照号（CN/HK 直接采用） |
| `VATNumber` | 否 | 50 | VAT 税号（非 CN/HK 优先采用；5-15 位字母数字） |
| `LegalPersonFullNamePinYin` | 是 | 200 | 法人拼音姓名（不能包含中文） |
| `LegalPersonPhone` | 是 | 50 | 法人电话（5-20 位，允许数字 / `+` / `-` / 空格 / 括号，兼容 `86-` 前缀） |
| `LegalPersonEmail` | 是 | 200 | 邮箱 |
| `LegalPersonBirthDate` | 是 | 20 | 出生日期，必须为合法日期，格式 `YYYY-MM-DD` |
| `CompanyAddressProvinceEn` | 是 | 100 | 省份英文（不能包含中文） |
| `CityEngName` | 是 | 100 | 城市英文（不能包含中文） |
| `LegalPersonCityEngName` | 是 | 200 | 法人出生地英文（不能包含中文） |
| `F_Mobile` | 否 | 15 | 销售电话（原值保存） |
| `LegalSignedFile` | 否 | 200 | 法人签名文件：URL 字符串，或 `[{"fileUrl":"...","fileName":"..."}]` JSON 数组字符串，原样透传给 RPA（不做源库附件查询；API 行**不**查 vat_db 附件表）；以 `[`/`{` 开头时必须为合法 JSON。**推荐传 API 桶相对路径**（如 `common-test/2026/08/xxx.png`），RPA 侧按 OSS 域名补全；**勿传 source 域名（`file.usaeu.com`）的完整 URL**——域名不匹配 API 桶时 RPA 原样直连、**跳过签名**，私有对象必然 403；**API 桶为私有桶，下载必须带签名**——RPA 取数时按 RPA 程序传入的腾讯云凭证生成 600s 预签名 URL（未传凭证则原样直连，公读对象仍可用，私有对象会 403） |

`bizParam.BusinessId` / `bizParam.BusinessSerialNumber` 最长 64 字符；超长字段聚合为一条 400（`字段长度超限: ...`）。

**VAT 解析规则**（与源流程共用）：
- `vat_number`：CN/HK 公司 → `RegNumber` 不变；其他国家 → 先取 `VATNumber`，为空回退 `RegNumber`，不加国家前缀
- `italy_vat_number`：仅非 CN/HK 公司写入；税号前两位为字母时去掉（`IT12345678901` → `12345678901`）

缺失字段聚合为一条 400 错误：`缺少必填字段: NameEng, CountryEn, ...`；格式错误同样聚合：`字段格式错误: NameEng(不能包含中文), LegalPersonBirthDate(必须为合法日期，格式 YYYY-MM-DD), ...`。

**受理落库**：受理成功即把业务字段（`company_name_en`、`company_address_en`、`vat_number`、`italy_vat_number`、法人姓名/生日拆分等）写入 `italia_epr_file` 对应列，并同时保留 `request_data` 原始 JSON；处理完成后再以转换后的完整结果覆盖（如意大利语国家名）。

### 1.3 受理响应

信封恒定：`{ "code", "msg", "ProcessMode": "async", "data", "bizParam" }`。

**凡经本接口信封返回的响应，HTTP 状态码恒为 200**（进程崩溃等未及应答的传输层错误不在此列，那类没有信封）。
业务成败与传输层守卫（含 `405` / `429`）一律**只由信封 `code` 表达**：`200`=生成文件成功，非 `200`=失败——
与 UNIFIED_API_DESIGN 及 app_withdrawn 的 `Response::json()` 一致，勿按 HTTP 状态分流。

| code | msg | 说明 |
|---|---|---|
| 200 | `success` | 受理成功且**说明书已在请求内生成完毕**；`data`=null，`bizParam` 原样回传；后续等 RPA 那一次统一回调下发两份文件 |
| 400 | 具体原因 | 见下方枚举 |
| 401 | `未授权` | 鉴权开启（`EPR_API_TOKEN_ENABLED=true`）且 Bearer token 缺失或不匹配（响应头 `WWW-Authenticate: Bearer`） |
| 405 | `仅支持 POST 请求` | 响应头 `Allow: POST` |
| 429 | `请求过于频繁，请稍后重试` | 超过 60 次/分/IP |
| 500 | `受理失败，请联系管理员` | 鉴权开启但 `EPR_API_TOKEN` 未配置等运行时错误 |
| 500 | `说明书生成失败（<阶段>），请联系管理员` | 受理内同步生成说明书失败；`<阶段>` ∈ `翻译` / `文档生成` / `OSS上传` / `数据处理`；`data`=`{"task_id":N}`。**该 task_id 不会再有回调**——失败已由本响应同步告知；调用方可修正后重投（终态行不挡重投） |

400 的 `msg` 枚举：
- `请求体为空或超过大小限制`
- `请求体不是有效的 JSON 对象`
- `仅支持 PushType=IT_EPR_REGISTER_FILE`
- `仅支持 Country=IT`
- `Data 和 bizParam 必须为对象`
- `bizParam.BusinessId（兼容 Businessld）和 BusinessSerialNumber 为必填`
- `缺少必填字段: A, B`（一次列出全部缺失字段）
- `字段长度超限: NameEng(最长200字符), ...`（一次列出全部超长字段；长度上限与目标表列宽一致，详见 1.2 表）
- `字段格式错误: NameEng(不能包含中文), LegalPersonBirthDate(必须为合法日期，格式 YYYY-MM-DD), ...`（一次列出全部格式错误字段；规则见 1.2 表）
- `重复请求：BusinessId {id} 或流水号 {code} 已有进行中的API任务，请勿重复提交`

## 2. 异步结果通知（回调）

- 回调地址：`.env` 的 `EPR_RESULT_CALLBACK_URL`（受理时写入行内）。**PHP 侧留空则不回调**（记 `notify_status=skipped`）；
  **RPA 侧**在行内地址为空时回退脚本内 `DEFAULT_CALLBACK_URL` 部署常量，故两侧都须指向 delivery 平台统一结果通知接口
  （生产地址见 UNIFIED_API_DESIGN §7.1；本机 `.env` 现为 `http://192.168.1.211:8080/delivery/rpa/callback`）
- 方式：`POST <callback_url>`，`Content-Type: application/json; charset=utf-8`
- 语义（对齐 UNIFIED_API_DESIGN §7.1）：**超时 10 秒、2xx 即送达（不解析响应体业务语义）、发送失败自动重试 3 次 / 间隔 5 秒**
  （重试只作用于 `epr:process-api` 兜底路径与 RPA 侧；**受理请求内不发任何回调**）
- **重试会产生重复 POST，接收端必须幂等**：成功回调按 `bizParam` + `data.task_id` 幂等；**失败回调 `data` 为 null（无 `task_id`），按 `bizParam` 幂等**（API 行的 `BusinessSerialNumber` 唯一）；重复通知覆盖式更新，以最后为准
- 回调只有**两个来源**，都是真异步、都没有调用方在等：`epr:process-api` 兜底路径与 RPA。
  受理请求内同步生成失败**不发回调**，直接由信封 `code=500` 告知（HTTP 恒 200；说明书是同步生成的，调用方此刻正阻塞在等这个响应）

### 2.1 成功回调

由 RPA 授权书写回后发送，`data.files` 含两份文件（说明书在前、授权书在后，空项跳过）：

```json
{
  "code": 200,
  "msg": "success",
  "ProcessMode": "async",
  "data": {
    "task_id": 123,
    "status": "success",
    "files": [
      {
        "url": "common-test/generatefile/2026/epr_italia/IT-2026-0001/Attestazione_....docx",
        "name": "Attestazione_....docx",
        "type": "EPR注册文件"
      },
      {
        "url": "common-test/generatefile/2026/epr_italia/IT-2026-0001/IT-2026-0001_授权书.pdf",
        "name": "IT-2026-0001_授权书.pdf",
        "type": "授权书"
      }
    ]
  },
  "bizParam": { "BusinessSerialNumber": "IT-2026-0001", "BusinessId": 789012 }
}
```

| `files[]` 字段 | 说明 |
|---|---|
| `url` | **OSS 相对路径**（不含域名、不含签名；域名由调用方拼接），形如 `{EPR_API_OSS_PREFIX}{年}/epr_italia/{流水号}/{文件名}`。API 行的行内 `desc_file_url` / `auth_file_url` 同样存相对路径（source 行仍为完整 URL） |
| `name` | 文件名（取自 `url` 末段） |
| `type` | 固定枚举：`EPR注册文件`（PHP 生成的说明书）、`授权书`（RPA 生成的授权书） |

> 说明书链接缺失时（历史行或异常情况）`files` 可能只含 `授权书` 一项，见 `api/italy_epr_callback_success_single_file.json`。

### 2.2 失败回调

`data` 恒为 `null`，错误原因放 `msg`：

```json
{
  "code": 500,
  "msg": "[文档生成] DocumentGenerationException: 模板文件不存在 (2026-08-17 10:00:00)",
  "ProcessMode": "async",
  "data": null,
  "bizParam": { "BusinessSerialNumber": "IT-2026-0001", "BusinessId": 789012 }
}
```

失败回调的**两处**触发点（均为真异步路径）：

1. **PHP 兜底命令**：`epr:process-api` 重试达 `EPR_API_DESC_MAX_RETRIES` → 置 `desc_status=3`（终态）+ 失败回调 + 企业微信
2. **RPA**：必填字段为空 / 认领次数达上限（`auth_count ≥ 30`）/ 授权书上传或回写失败 → `auth_status=3` + 失败回调

> 受理内同步生成失败**不在**此列：它改由信封 `code=500` 同步返回（HTTP 恒 200，见 §1.3），不发回调。

`task_id` 为 target 库 `italia_epr_file` 表主键。回调投递明细落 `notify_status` / `notify_attempts` / `notify_request` / `notify_response` / `notify_time` 列（列未迁移时静默跳过）。

## 3. 处理流程与状态机

```
受理（POST /api/epr/italy，同步 200/400）
  → INSERT italia_epr_file：data_source='api', desc_status=1（处理中，排除 worker 竞争）,
                            auth_status=0, callback_url=env, biz_param=原样 JSON
                            （callback_url 写入行内是**给 RPA 读**的；受理路径自身不发回调）
  → 受理内同步生成「EPR 注册文件」（同一管线：转换→翻译→docx→COS）
      ├ 成功 → desc_status=2 + desc_file_url（OSS 键 {前缀}{年}/epr_italia/{流水号}/）
      │        → 受理返回 200 + data=null
      └ 失败 → desc_status=3 + desc_error_msg → 企业微信
               → 受理返回 code=500 + 分级原因 + data={task_id}（HTTP 恒 200，**不发回调**）
  → RPA rpa_get 认领（auth_status 0→1）
      API 行：不查 vat_db 附件表，签名文件直接取接口传值
      必填字段空 / 认领次数达上限 → auth_status=3 + 失败回调
  → RPA 生成授权书 → rpa_save：
      API 行 → 授权书上传 {前缀}{年}/epr_italia/{流水号}/
               auth_status=2 + auth_file_url；不写 Base_AnnexesFile、不更新 EPRRegInfo
               → 成功回调（data.files 两份文件）
      source 行 → 附件入库 + EPRRegInfo.PushTaxBureauStatus=6/7（无回调，行为与历史一致）
```

- **EPR 注册文件在受理请求内同步生成**（与源流程"数据同步后即生成文件"一致），不依赖 `epr:process-api` 轮询；
  `epr:process-api` 仅保留为兜底：回收历史遗留的 `desc_status=0` 行与陈旧 `desc_status=1` 行（`EPR_API_DESC_MAX_RETRIES` 仅作用于该兜底路径）
- **调用方只需记住一条规则**：信封 `code=200` → 等 RPA 那一次统一回调；`code=500` → 该 `task_id` **不会再有回调**
- **受理中途崩溃**（INSERT 后、终态回写前）：行停在 `desc_status=1`，10 分钟后被 `epr:process-api` 兜底回收；若兜底重试耗尽会**发失败回调**——调用方可能先收到连接错误/传输层 500、后收到失败回调。语义正确：前者是传输层结果（无信封），回调是任务终态，且回调按 `bizParam` 幂等；**勿与 §1.3 的信封 `code=500` 混为一谈**
- 受理落库即写入当天 `doc_day` / `doc_month` / `doc_year`
- 授权书（auth）**不**在受理阶段生成：由 RPA 程序读取数据生成后经 `python/rpa_save.py` 回写并回调

API 任务**全程不读不写 source 库**（不查询附件表、不更新 EPRRegInfo 状态）。

## 4. 配置项（.env）

```ini
EPR_API_TOKEN_ENABLED=false       # 鉴权开关（false=跳过 token 验证；true=开启验证）
EPR_API_TOKEN=                    # Bearer token（仅开关开启时校验；开启且留空时接口返回 500）
EPR_API_MAX_BODY_BYTES=2097152    # 请求体大小上限
EPR_RESULT_CALLBACK_URL=https://test-cloud.usaeu.com/prod-api/delivery/rpa/callback  # 异步结果回调地址（留空不回调）
EPR_RESULT_CALLBACK_TIMEOUT=10    # 回调 HTTP 超时秒数
EPR_RESULT_CALLBACK_MAX_RETRIES=3 # 回调发送失败重试次数（总尝试 = 重试次数 + 1）
EPR_RESULT_CALLBACK_RETRY_DELAY=5 # 回调重试间隔秒数
EPR_RESULT_CALLBACK_VERIFY_SSL=false  # 回调 HTTPS 证书校验（默认关闭：回调域名证书链含内网自签 CA）
EPR_API_COS_BUCKET=usaeu-1259285998   # API 行 COS 桶（留空=与 TENCENT_COS_BUCKET 同桶）
EPR_API_OSS_PREFIX=common-test/generatefile/  # API 行 COS 相对路径前缀（须与 RPA 常量一致）
EPR_API_POLL_INTERVAL=8           # API 处理器轮询间隔
EPR_API_DESC_MAX_RETRIES=3        # 说明书阶段最大重试次数（仅兜底路径）
```

> **三份常量**：RPA 进程不读 `.env`，`python/rpa_save.py` 顶部的
> `API_FLOW_BUCKET_DEFAULT` / `API_FLOW_OSS_PREFIX` / `DEFAULT_CALLBACK_URL`
> 必须分别与 `.env` 的 `EPR_API_COS_BUCKET` / `EPR_API_OSS_PREFIX` / `EPR_RESULT_CALLBACK_URL` 一致，
> 否则 PHP 与 RPA 各自传到不同桶 / 前缀，或回调发到错误地址（两份文件必须落在同一桶同一目录）。

## 5. 数据库变更

上线前先在 target 库执行 `database/italia_epr_file_api_columns.sql`（幂等，可重复执行）：
新增 `data_source`、`biz_param`、`request_data`、`callback_url`、`desc_count` 列，
以及回调投递追溯列 `notify_status`（CHECK：`skipped`/`success`/`failed`）、`notify_request`、`notify_response`、`notify_attempts`、`notify_time`；
并防御性补齐 `auth_count`、`auth_file_id`、`desc_file_id`；
同时创建 API 行流水号唯一索引 `UX_italia_epr_file_api_code`（`code` WHERE `data_source='api'`，防重复受理并保证插入重试幂等——若库中已存在重复 API 行需先清理）。

## 6. 样例

- 请求样例：[api/italy_epr_request.json](api/italy_epr_request.json)
- 成功回调样例（两份文件）：[api/italy_epr_callback_success.json](api/italy_epr_callback_success.json)
- 成功回调样例（仅授权书）：[api/italy_epr_callback_success_single_file.json](api/italy_epr_callback_success_single_file.json)
- 失败回调样例：[api/italy_epr_callback_failure.json](api/italy_epr_callback_failure.json)
- 对接文档中央仓库 `app_withdrawn/docs/` 下同步维护：测试数据 `意大利EPR注册.json`、回调样例 `意大利EPR注册回调.json`，
  以及 `UNIFIED_API_DESIGN.md` §4.4.6（意大利 EPR 注册）、§5.2（`type` 枚举）、§5.4（文件槽）、§7（回调契约）

## 7. 注意事项

- **部署顺序**：ALTER 脚本必须先行于任何 PHP / Python 代码上线
- **RPA 程序**：API 行生效要求 RPA 程序把 `rpa_get.py` 返回行中的 `data_source` / `callback_url` 透传给 `rpa_save.py`（参数有默认值，源流程不受影响）
- **单实例假设**：`epr:process-api` 与 `epr:process-italy` 各一个 NSSM 实例，双实例会重复认领
- **防重规则**：同 `BusinessId` 或同流水号存在进行中的 API 任务（desc 未终态，或 desc 已完成但授权书未终态）→ 400；终态后允许重新提交
- **重新提交复用同一 `task_id`**：唯一索引只按流水号（`code`）过滤、不含状态，故终态（`desc_status=3`）后重投会复用原行并以本次 payload 刷新，`task_id` 不变——便于双方对账，但**不要**用它去重（同一 `task_id` 可能对应多次提交）
- **回调幂等**：回调可能重复投递（兜底命令与 RPA 两侧均有重试），接收端按 `bizParam` + `task_id` 幂等
