# 法国 LEKO 注册提交（注册邮件）接口文档

> **对接契约来源**：本文档描述本项目（`fr_epr_reg`）实现侧的接口契约。
> 完整对接契约以新系统仓库文档为准：
> `E:\ou\meiou-app\app_withdrawn\docs\UNIFIED_API_DESIGN.md` §4.4.23。
>
> **同族接口**：生成文件（[`french-file-generation-api.md`](french-file-generation-api.md)）→
> 合并文件（[`french-merge-xlsx-api.md`](french-merge-xlsx-api.md)）→ **注册提交（本文）** →
> 下号（UIN，RPA 流程，本服务不覆盖）。

## 接口地址

**POST** `/fr_epr_reg/api/epr/fr/registration-mail`

服务器地址：`http://automation.usaeu.com:8888/`

完整地址：`http://automation.usaeu.com:8888/fr_epr_reg/api/epr/fr/registration-mail`

## 功能说明

供新系统（SaaS）同步调用：按 OSS key 从 API 桶取回**合并后的公司清单 XLSX** 与**各公司 POA PDF**，
服务端组装 `POA.zip`，校验「XLSX 第 14 行起数据行数 == POA 数量」后，发送注册邮件给 Léko
（收件人 `SMTP_MAIL_TO`，默认 `myleko@leko-organisme.fr`）。

- 与老接口（`POST /api/epr/send-registration-mail`，source 流程）的差异：入参为 **API 桶 OSS 相对路径**
  （而非 `xlsx_url` + `pdf_zip_base64` + `EPRRegInfo.Id`）；**不回写** `PushTaxBureauStatus`
  （source 流程会置 3），**不读写任何数据库**。
- 附件固定名（Léko 侧按固定名收件）：`Leko_Template list of companies_v3.11 {年}.xlsx` + `POA.zip`；
  **不使用**调用方传入的 `name` 作附件名。
- 注册完成后的**下号（UIN）由 RPA 流程执行**（Python 脚本，另见项目 RPA 目录），本接口只负责提交。

## 访问控制

本接口**不受** `internal.network`（172.16.x.x）限制，与生成/合并接口共用同一组守卫：

| 中间件 | 作用 | 失败响应 |
|---|---|---|
| `fr.api.transport` | 仅接受 POST + `Content-Type: application/json`；请求体上限 `FR_FILE_API_MAX_BODY_BYTES`（默认 1 MB） | 405 / 400 |
| `fr.api.bearer` | 可选 Bearer 鉴权，`FR_FILE_API_AUTH_ENABLED` 控制（默认 `false`） | 401 |
| `fr.api.rate` | 按客户端 IP 限流，`FR_FILE_API_RATE_LIMIT_PER_MINUTE`（默认 60） | 429 |

> **限流配额**：生成 / 合并 / 注册三个接口**共享同一每 IP 配额**（60/分钟）。

## 请求

### Content-Type

`application/json`

### 请求参数（统一信封）

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| PushType | string | 是 | 固定 `FR_EPR_REGISTER_LEKO`；白名单精确分派 |
| Country | string | 是 | 业务国，**必须恰为 `FR`** |
| Data.files | array | 是 | 文件数组：**恰好 1 条 `EPR业务申请表`** + **1~100 条 `授权书`** |
| Data.files[].url | string | 是 | **API 桶 OSS 相对路径**（生成/合并接口的 `data.files[].url`） |
| Data.files[].name | string | 否 | 用作 `POA.zip` 内条目名（经净化，见下）；缺省时按 `POA-{序号}` |
| Data.files[].type | string | 是 | `EPR业务申请表`（合并产物）或 `授权书`（POA PDF）——注册流程必须区分 |
| bizParam | object | 是 | 透传参数；成功与失败响应均**原样回传** |

### OSS key 规则（安全校验）

与合并接口一致：非空、非绝对 URL、无 `\`/控制字符、无 `.`/`..` 段（含编码绕过）、
必须匹配 `{OSS_API_PREFIX}{4位年}/{OSS_API_MODULE_DIR}/`、对象必须存在；单文件/合计大小上限见
`FR_FILE_API_OSS_MAX_FILE_BYTES`（20 MB）/ `FR_FILE_API_OSS_MAX_TOTAL_BYTES`（200 MB）。

### POA.zip 条目名规则

条目名 = `FileNameSanitizer::sanitize(pathinfo(name, FILENAME))` + `.pdf`：
剔除 `\ / : * ? " ' < > | # %`（保留 `&`），强制 `.pdf` 扩展名（条目计数按 POA 条数，与扩展名无关），
重名追加 `-{序号}`（仍冲突则继续递增序号，保证条目名唯一）；`name` 缺失或净化后为空时用 `POA-{序号}`。

### 请求示例

```json
{
    "PushType": "FR_EPR_REGISTER_LEKO",
    "Country": "FR",
    "bizParam": {
        "BusinessSerialNumber": "FREPR2026000001"
    },
    "Data": {
        "files": [
            {
                "url": "common-test/generatefile/2026/fr_epr_reg/merged/merged_20260912_101500_a1b2c3d4.xlsx",
                "name": "merged_20260912_101500_a1b2c3d4.xlsx",
                "type": "EPR业务申请表"
            },
            {
                "url": "common-test/generatefile/2026/fr_epr_reg/leko/FREPR2026000001/POA-EXAMPLE COMPANY SAS.pdf",
                "name": "POA-EXAMPLE COMPANY SAS.pdf",
                "type": "授权书"
            },
            {
                "url": "common-test/generatefile/2026/fr_epr_reg/leko/FREPR2026000002/POA-SECOND COMPANY SAS.pdf",
                "name": "POA-SECOND COMPANY SAS.pdf",
                "type": "授权书"
            }
        ]
    }
}
```

## 响应

### 成功响应（200）

```json
{
    "code": 200,
    "msg": "success",
    "ProcessMode": "sync",
    "data": {
        "files": [],
        "sent_to": "myleko@leko-organisme.fr",
        "client_count": 2
    },
    "bizParam": {
        "BusinessSerialNumber": "FREPR2026000001"
    }
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| data.files | array | 固定空数组 `[]`（本流程不上传文件） |
| data.sent_to | string | 实际收件人（`SMTP_MAIL_TO`） |
| data.client_count | int | 本次注册的公司数（= POA 数量 = XLSX 数据行数） |
| bizParam | object | 请求 `bizParam` 原样回传 |

### 校验失败（400）

```json
{
    "code": 400,
    "msg": "Validation failed: Data.files must contain exactly one EPR业务申请表 entry",
    "data": null
}
```

### 业务失败（400）

| 类别 | 触发场景 |
|------|----------|
| key 违规（聚合校验错误） | key 为空 / 绝对 URL / 目录穿越 / 不在本流程 API 目录内——随其余字段错误聚合返回，`msg` 为 `Validation failed: …`，非独立分类码 |
| `OSS_KEY_NOT_FOUND` | OSS 对象不存在 |
| `OSS_FILE_TOO_LARGE` | 单文件或合计字节数超上限 |
| `OSS_DOWNLOAD_FAILED` | 从 API 桶读取失败 |
| `COUNT_MISMATCH` | XLSX 数据行数与 POA 数量不一致（**不发邮件**） |
| `MAIL_SEND_FAILED` | POA.zip 组装或 SMTP 发送失败（明细只进日志） |

### 传输 / 鉴权 / 限流 / 未分类失败

| HTTP | 场景 |
|------|------|
| 405 | 非 POST 方法 |
| 400 | Content-Type 非 `application/json`，或请求体超限 |
| 401 | `FR_FILE_API_AUTH_ENABLED=true` 且 Bearer 缺失/错误 |
| 429 | 超出每 IP 每分钟配额（三接口共享） |
| 500 `Internal error` | 未分类异常；不回显内部消息，仅记录异常类名 |

## 实现细节

- 编排：`FrenchRegistrationMailService`（校验 → 逐 key 下载 → 组装 POA.zip → 行数一致性 → 发信）。
- 邮件发送与行数统计：共享 `LekoRegistrationMailer`（与 source 流程 `RegistrationMailService`
  同一实现：主题/正文/附件名/SMTP 配置完全一致；行数统计走 `ZipArchive + XMLReader` 轻量实现）。
- **零数据库访问**：服务不注入任何 Repository；邮件发送后不写任何状态。
- 日志：`api_french` 通道（push_type、POA 数量、行数校验结果、sent_to、client_count）。

## cURL 调用示例

```bash
# xlsx_url 用合并接口产物；POA key 用生成接口返回的授权书项
curl -X POST \
  http://automation.usaeu.com:8888/fr_epr_reg/api/epr/fr/registration-mail \
  -H 'Content-Type: application/json' \
  -d '{
    "PushType": "FR_EPR_REGISTER_LEKO",
    "Country": "FR",
    "bizParam": {},
    "Data": {"files": [
      {"url": "common-test/generatefile/2026/fr_epr_reg/merged/merged_20260912_101500_a1b2c3d4.xlsx", "type": "EPR业务申请表"},
      {"url": "common-test/generatefile/2026/fr_epr_reg/leko/FREPR2026000001/POA-EXAMPLE COMPANY SAS.pdf", "type": "授权书"}
    ]}
  }'
```

启用 Bearer 时追加请求头 `-H 'Authorization: Bearer <FR_FILE_API_AUTH_TOKEN>'`。
