# EPR 注册邮件发送接口文档

## 接口地址

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

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

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

## 功能说明

将法国 EPR 注册的合并 XLSX 文件和 PDF ZIP 包通过邮件发送给 Léko，同时更新源数据库中对应记录的推送状态为"推送成功"（PushTaxBureauStatus=3）。

发送前会校验 XLSX 数据行数、PDF 文件数量与提交 ID 数量三者一致，不一致则拒绝发送。

## 访问控制

接口受 `internal.network` 中间件保护，仅允许内网（172.16.x.x 子网）请求访问，非内网请求返回 401。

> **注意**：IP 限制默认开启（生产环境，由 `INTERNAL_NETWORK_ENFORCE` 控制，默认 `true`）。本地开发机不在 172.16 子网时，可在 `.env` 设置 `INTERNAL_NETWORK_ENFORCE=false` 临时关闭。

同时受 `parse.large.json` 中间件处理，支持大体积 JSON 请求体（含 Base64 编码的 PDF ZIP），自动处理 JSON 中 Base64 换行符导致的解析失败。

## 请求

### Content-Type

`application/json`

### 请求参数

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| xlsx_url | string | 是 | 合并后 XLSX 文件的腾讯云 COS 访问地址，必须是合法 URL |
| pdf_zip_base64 | string | 是 | 包含所有 PDF 证书的 ZIP 文件的 Base64 编码字符串，最大 67MB |
| epr_reg_info_ids | string[] | 是 | `EPRRegInfo.Id` 数组，1~100 个 UUID 字符串（最长36位） |

### 请求示例

```json
{
    "xlsx_url": "https://cos-xxx.myqcloud.com/epr_factory/fr/merged/merged_20260507_143000_abcd1234.xlsx",
    "pdf_zip_base64": "UEsDBBQAAAAIAA...",
    "epr_reg_info_ids": [
        "EPR-REG-INFO-ID-001",
        "EPR-REG-INFO-ID-002",
        "EPR-REG-INFO-ID-003"
    ]
}
```

## 响应

### 成功响应（200）

```json
{
    "code": 200,
    "msg": "Registration mail sent successfully",
    "data": {
        "sent_to": "contact@leko.fr",
        "client_count": 3
    }
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| code | int | 状态码，200 表示成功 |
| msg | string | 响应消息 |
| data.sent_to | string | 邮件收件人地址（来自 SMTP 配置 `services.smtp.to_email`） |
| data.client_count | int | 发送的客户数量（即 `epr_reg_info_ids` 数组长度） |

### 参数校验失败（400）

```json
{
    "code": 400,
    "msg": "xlsx_url is required",
    "data": null
}
```

其他可能的校验错误消息：

| 触发条件 | msg 内容 |
|----------|----------|
| xlsx_url 为空 | `xlsx_url is required` |
| xlsx_url 不是合法 URL | `xlsx_url must be a valid URL` |
| pdf_zip_base64 为空 | `pdf_zip_base64 is required` |
| epr_reg_info_ids 为空 | `epr_reg_info_ids is required` |
| epr_reg_info_ids 不是数组 | `epr_reg_info_ids must be an array` |
| epr_reg_info_ids 为空数组 | `epr_reg_info_ids must contain at least 1 ID` |
| epr_reg_info_ids 超过100个 | `epr_reg_info_ids must not exceed 100 IDs` |
| 单个 ID 为空 | `EPRRegInfo ID cannot be empty` |

### 业务校验失败（400）

```json
{
    "code": 400,
    "msg": "XLSX data rows(5) and PDF count(3) mismatch",
    "data": null
}
```

其他可能的业务错误消息：

| 触发条件 | msg 内容 |
|----------|----------|
| XLSX rows ≠ PDF count | `XLSX data rows(N) and PDF count(M) mismatch` |
| XLSX rows ≠ ID count | `XLSX data rows(N) and ID count(M) mismatch` |
| ID count exceeds 100 | `Count exceeds limit, max 100, got N` |
| XLSX download failed | `File download failed(HTTP 404): {url}` |
| Base64 decode failed | `PDF ZIP base64 decode failed` |
| ZIP open failed | `ZIP file open failed (error code: N)` |
| XLSX open failed | `XLSX file open failed (error code: N)` |
| XLSX worksheet XML not found | `XLSX worksheet XML not found` |
| Mail send failed | `Mail send failed: {PHPMailer error details}` |

### 非内网访问（401）

```json
{
    "code": 401,
    "msg": "Unauthorized: internal network only",
    "data": null
}
```

## 参数校验规则

| 规则 | 说明 |
|------|------|
| xlsx_url 必填 | 请求体中必须包含此字段 |
| xlsx_url 字符串类型 | 必须为字符串 |
| xlsx_url 合法 URL | 必须是有效的 URL 格式 |
| pdf_zip_base64 必填 | 请求体中必须包含此字段 |
| pdf_zip_base64 字符串类型 | 必须为字符串 |
| pdf_zip_base64 最大67MB | 最大长度 67000000 字符 |
| epr_reg_info_ids 必填 | 请求体中必须包含此字段 |
| epr_reg_info_ids 数组类型 | 必须为 JSON 数组 |
| epr_reg_info_ids 1~100个 | 数组长度 1~100 |
| epr_reg_info_ids.* 必填 | 数组中每个元素不能为空 |
| epr_reg_info_ids.* 字符串类型 | 每个元素必须是字符串 |
| epr_reg_info_ids.* 最大36位 | 每个元素最长 36 个字符（UUID 长度） |

## 处理流程

1. **下载 XLSX**：从 `xlsx_url` 下载合并后的 XLSX 文件到本地临时目录
2. **解码 ZIP**：将 `pdf_zip_base64` 解码为二进制数据，保存为本地 ZIP 临时文件
3. **统计 XLSX 行数**：读取 XLSX Sheet1，从第14行开始统计非空数据行数（A-AJ列）
4. **统计 PDF 数量**：打开 ZIP，统计 `.pdf` 文件数量（排除目录项）
5. **数量校验**：XLSX 行数 = PDF 数量 = `epr_reg_info_ids` 数量，三者不一致则报错
6. **发送邮件**：
   - 邮件主题：`Register with Léko {年份} for {客户数} clients--Sea&Mew Consulting GmbH--{日期}`
   - 邮件正文（HTML 格式，保留段落换行）：
     ```
     Hello,

     I would like to help {客户数} clients register with Léko for package in {年份}, please find attached the documents required for registration.

     If you have any questions, please do not hesitate to contact us.

     Have a good day.

     Best regards
     Sea&Mew Consulting GmbH
     Email: info@seamew.de
     ```
   - 附件1：XLSX 文件，文件名 `Leko_Template list of companies_v3.11 {年份}.xlsx`
   - 附件2：PDF ZIP 包，文件名 `POA.zip`
   - SMTP 配置：阿里云企业邮箱，SSL 加密，收件人地址来自 `services.smtp.to_email`
7. **更新状态**：将所有 `epr_reg_info_ids` 对应的源记录 `PushTaxBureauStatus` 更新为 3（推送成功）
8. **清理临时文件**：删除本地 XLSX 和 ZIP 临时文件

## 邮件发送配置

| 配置项 | 来源 | 说明 |
|--------|------|------|
| SMTP 主机 | `services.smtp.host` | 阿里云企业邮箱服务器 |
| SMTP 端口 | `services.smtp.port` | SSL 端口 |
| 发件人 | `services.smtp.from_email` / `from_name` | Sea&Mew Consulting GmbH |
| 收件人 | `services.smtp.to_email` | Léko 接收邮箱 |
| 加密方式 | SMTPS (SSL) | PHPMailer `ENCRYPTION_SMTPS` |
| 字符编码 | UTF-8 | |

## 邮件主题格式

`Register with Léko {year} for {count} clients--Sea&Mew Consulting GmbH--{Y.m.d}`

示例：`Register with Léko 2026 for 3 clients--Sea&Mew Consulting GmbH--2026.05.07`

## 数量校验说明

三者一致性校验是关键安全措施：

- **XLSX 数据行数**：从第14行开始逐行扫描 A-AJ 列，任一列有非空值即计为一行
- **PDF 文件数量**：ZIP 内所有 `.pdf` 扩展名的文件（排除目录项）
- **提交 ID 数量**：`epr_reg_info_ids` 数组长度

三者不一致意味着数据不完整或存在遗漏，拒绝发送并返回具体不一致的数值。

## XLSX 内存管理

统计 XLSX 行数时临时将 `memory_limit` 提升至 512M（PhpSpreadsheet 需求），统计完成后恢复原值并释放工作表对象。

## 注意事项

- 请求体可能因 Base64 编码含换行符而体积较大，`parse.large.json` 中间件会自动处理 JSON 解析问题
- Windows 环境下阿里云企业邮箱 SSL 证书可能不匹配，代码已跳过 SSL 证书验证
- 邮件正文以 HTML 格式发送（`isHTML(true)`），换行通过 `nl2br()` 转为 `<br>` 标签，AltBody 保留纯文本格式
- 处理完成后所有临时文件（XLSX 下载、ZIP 解码）都会被清理
- 错误信息记录在 `api_reg_mail` 日志通道，路径为 `storage/logs/api/reg-mail-YYYY-MM-DD.log`（30 天保留，`LOG_API_DAYS` 环境变量）
- 发送成功后源记录状态更新为推送成功（PushTaxBureauStatus=3），不可重复发送

## cURL 调用示例

```bash
curl -X POST \
  http://automation.usaeu.com:8888/fr_epr_reg/api/epr/send-registration-mail \
  -H 'Content-Type: application/json' \
  -d '{
    "xlsx_url": "https://cos-xxx.myqcloud.com/epr_factory/fr/merged/merged_20260507_143000_abcd1234.xlsx",
    "pdf_zip_base64": "UEsDBBQAAAAIAA...",
    "epr_reg_info_ids": [
      "EPR-REG-INFO-ID-001",
      "EPR-REG-INFO-ID-002",
      "EPR-REG-INFO-ID-003"
    ]
  }'
```