# 对外接收接口（api.php）

## 概述

`api.php` 是英国 VAT 注册数据接收接口：接收外部系统（SaaS）推送的注册业务数据，**不读取 source 库**，校验并转换后写入目标库 `uk_vat_register` 表，注册邮箱经 `EmailService` 自动创建——**test 环境不新建邮箱**（邮箱服务器无 test/prod 之分，测试新建会在共享邮件服务器上消耗正式邮箱），固定使用 `registrationvat19413@usaeu.com`。处理规则与 `bin/sync.php` 一致（同一校验器 `DataValidator`、同一转换规则 `VatRecordTransform`）。

统一接口契约见 `app_withdrawn` 仓库 `docs/UNIFIED_API_DESIGN.md` §4.4.8；示例请求数据为同仓库 `docs/英国VAT注册.json`。

## 接口信息

| 项目 | 值 |
| ---- | ---- |
| 地址 | `POST {部署路径}/api.php` |
| 环境 | 由 `api.php` 顶部 `API_ENVIRONMENT` 常量配置（仅支持 `test` / `prod`）；请求不能切换环境 |
| 请求 | `Content-Type: application/json`，体为 `{PushType, Country, bizParam, Data}`，最大 2 MB |
| 鉴权 | `api.php` 顶部 `API_KEY` **非空**时校验 `X-API-Key` 请求头（缺失或不匹配返回 401）；**留空则关闭鉴权**（内网部署可留空） |
| 当前支持 | `PushType=GB_VAT_REGISTER`，`Country=GB`（大小写不敏感） |

## 响应格式

统一信封 `{code, msg, ProcessMode, data, bizParam}`（HTTP 状态码与 `code` 一致）：

| code | 含义 |
| ---- | ---- |
| 200 | 成功；`data=null`（统一信封不携带任何数据，仅 `code`/`msg`/`bizParam`，与非 200 响应一致） |
| 400 | 信封或字段校验失败；`msg` 为中文提示一次性聚合全部错误（`; ` 分隔），接口字段名在括号中标注（如 `公司名称为必填项（NameEng）`） |
| 401 | `API_KEY` 非空时，`X-API-Key` 缺失或不匹配 |
| 405 | 非 POST 请求 |
| 409 | 相同业务流水号正在处理中，或目标记录 `registration_status=1` 处理中 |
| 413 | 请求体超过 `max_body_bytes`（默认 2MB） |
| 500 | 服务器内部错误（细节仅写日志） |

> **所有响应 `data` 统一为 `null`**（200 成功与 400/401/405/409/413/500 均不携带数据，仅 `code`/`msg`/`bizParam`；如需核验落库结果请按 `bizParam` 对接）。

## 请求示例

```bash
# api.php 顶部 API_KEY 非空时必带 X-API-Key；留空鉴权时可省略该行
curl -X POST "http://127.0.0.1:8099/api.php" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: [api_key]" \
  --data-binary @英国VAT注册.json
```

成功响应：

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

## Data 字段说明

> 完整字段契约见 `app_withdrawn` 仓库 `UNIFIED_API_DESIGN.md` §4.4.8（GB_VAT_REGISTER）。本接口按新契约处理：

| 数据字段 | 说明 |
| ---- | ---- |
| `Country` / `LegalPersonCountry` | **只传国家二字码**（如 `CN`/`HK`，禁传中文名/英文名——旧格式 `中国`/`China` 直接报数据错误）。系统从 `cache/countries.json` 国家字典解析英文名落库（`office_country`/`legal_person_country`），字典原始名（GB=`United Kiongdom`、HK=`Hongkong` 等）归一为目标库规范名（`United Kingdom`/`Hong Kong` 等），与 source 流程写入值一致 |
| `LegalPersonNamePinyin` | 法人英文名（拼音，**名**），与姓成对必填 |
| `LegalPersonSurnamePinyin` | 法人英文姓（拼音，**姓**），与名成对必填 |
| `UploadFile` | **上传文件列表（必填）**：值 `[{fileUrl, fileName}]`（JSON 数组字符串或原生数组，可多条），全部附件放同一字段；文件名（小写）包含关键词即命中，分类规则与 source 流程（`FileService`）一致——法人证件（`passport`/`identity card`/`drive license`）→ `upload_file_path1`；支持文件（关键词见下）→ 按序落 `upload_file_path2`、`upload_file_path3`（匹配多个则多个一并递交，仅一个支持文件时 path3 留空） |
| ~~`LegalPersonFullNamePinYin`~~ | **已废弃，不再接受**（source 流程内部仍使用该字段） |

> **文件字段说明**：字段值为统一格式 `[{fileUrl, fileName}]`（JSON 数组字符串或原生数组，`fileUrl` 仅支持 https 或内部文件服务相对路径；**绝对 https 地址提交时会做 HEAD 探测（5s 超时）确认文件真实存在**，网络错误/HTTP ≥400 报 400「文件不存在或无法访问」，内部相对路径离线无法验证、跳过探测）。分类规则与 source 流程（`FileService`）完全一致：文件名（小写）包含关键词即命中，**匹配多个则多个文件一并递交**——法人证件第一个命中 → `upload_file_path1`；支持文件按序落入 `upload_file_path2`、`upload_file_path3`（首个填 path2、次个填 path3），只有一个支持文件时 path3 留空。以下情况报数据错误（400）：文件名未命中任何关键词；文件中条目缺少 `fileUrl` 或字段非法 JSON；数量超容量（法人证件 >1 或支持文件 >2，超出部分无法递交）。旧字段名 `LegalPersonIdDocumentPic`/`SupportingDocumentPic1`/`SupportingDocumentPic2` 及旧版 `UploadFile1`/`UploadFile2`/`UploadFile3` 均已废弃不再接受：提交后一律视为 `UploadFile` 未传，直接报 400「缺少上传文件字段（UploadFile）」（与未传新字段相同）。
>
> **支持文件关键词**：bank statement（银行凭证）/ credit card statement（信用卡流水）/ electricity bill（电费账单）/ water bill（水费账单）/ property fee bill（物业费账单）/ telephone bill（话费账单）/ gas bill（燃气费）/ broadband bill（宽带账单）/ property ownership certificate（房产证）/ birth certificate（户口本）/ social security certificate（社保凭证）。

## 防重与状态语义

**查重键 `bizParam.BusinessId` 直接存入 `source_record_id = "API:" + BusinessId`**（同一客户/业务单的稳定标识，≤45 字符；等值查询 + `DataSource='api'` 过滤唯一索引兜底）；BusinessId 缺失时降级为 `source_record_id = "API:" + BusinessSerialNumber`（`BusinessSerialNumber` 必填，≤45 字符）。查重先按 `source_record_id` 等值，未命中再经 `biz_param` 内 BusinessId 的 JSON 提取兜底（兼容升级前旧行，多行命中取最老记录为规范行）。

> `code` 列写入 `bizParam.BusinessSerialNumber`（与 `biz_param` 同源；`Data.Code` 不再写入 code 列）。RPA 四脚本（`rpa_step1_get.py`/`rpa_step1_save.py`/`rpa_step2_mtdbind_save.py`/`rpa_step3_eori_save.py`）的 `parse_biz_param()` 在 `biz_param` 列缺失/损坏时按 `code` 列回取 `BusinessSerialNumber` 兜底。

按目标库 `registration_status` 状态分派（0 待注册 / 1 处理中 / 2 注册成功 / 3 注册失败 / 4 重试达上限）：

| 状态 | 含义 | 重复提交行为 |
| ---- | ---- | ---- |
| 不存在 | — | INSERT（`registration_status=0`；邮箱：test 环境固定 `registrationvat19413@usaeu.com`，prod 环境创建新邮箱） |
| `0` | 待注册 | **409 报错**（`注册待处理，不允许重复提交`），不更新、不建邮箱 |
| `1` | 处理中（RPA 领取） | **409 报错**（`注册正在处理中`），不更新、不建邮箱 |
| `2` | 注册成功 | **409 报错**（`注册已完成，不允许更新`），不允许更新或重复注册 |
| `3` | 注册失败 | **允许更新资料**：UPDATE（复用已有邮箱，状态与失败计数重置为 0） |
| `4` | 重试达上限 | **允许更新资料**：UPDATE（同 `3`，避免业务锁死） |

> 成功态（2）为 409 报错而非 200 跳过（防重复提交契约）；失败态（3/4）重复提交可修复资料并发起重试。BusinessId 缺失时降级按流水号查重，状态矩阵不变。

> 数据库执行顺序：先在正式库 `rpa` 执行 `sql/add_data_source_and_api_fields.sql`，该脚本只增量追加 `DataSource`、`biz_param`、约束和 API 唯一索引，不修改其他正式字段；增量迁移脚本（`sql/add_*.sql`）正式库 `rpa` 与测试库 `rpa_test` 均可直接执行（幂等）。测试库也可执行 `sql/rebuild_rpa_test_uk_vat_register.sql` 以同实例正式表实时结构重建测试表——会清空原测试数据，仅在需要重置测试库时使用。
>
> ⚠ **部署顺序（硬性）**：必须先执行 `sql/add_data_source_and_api_fields.sql`（正式库 rpa），**再**部署/重启 `bin/sync.php` 与 `api.php`——新代码的 SQL 已引用 `DataSource`/`biz_param` 列，旧库直接运行会报错。异步结果通知所需 `Notify1*`/`Notify2*` 两套列（每轮一套，共 10 列）另需执行 `sql/add_notify_fields.sql`（新增列的写入方为 RPA 三步保存脚本，旧库未迁移时仅告警不中断）。

**并发安全**：同一 BusinessId（缺省时同一流水号）使用 SQL Server `sp_getapplock` 串行处理，并由 `DataSource='api'` 的过滤唯一索引兜底，因此同一客户换流水号并发提交也不会并发创建多个注册邮箱。

**注册邮箱与环境**：`API_ENVIRONMENT = test` 时不调用邮箱创建 API、不消耗 `email_sequence_manager` 序号（邮箱服务器无 test/prod 之分），`customer_email` 固定写入 `registrationvat19413@usaeu.com`；仅在 `prod` 环境经 `EmailService` 正常创建 `registrationvat{N}@usaeu.com`。已有行的邮箱始终复用（不因环境切换替换）。

## 异步结果通知（仅 API 行）

API 行（`DataSource='api'`）受理后由 RPA 流程处理（`python/rpa_step1_get.py` 领取、`python/rpa_step1_reg_download.py` 下载上传文件、`python/rpa_step1_save.py` 注册完成保存、`python/rpa_step2_mtdbind_save.py` 绑定 MTD、`python/rpa_step3_eori_save.py` 申请 EORI），处理结果通过 **delivery 平台统一异步结果通知接口**通知调用方（与 ES 海牙 030 / 意大利 EPR 同模式）。**source 行完全不同的处理：step1 走老 open-token 回调上报注册结果（runner 传 `callback_url`/`callback_token`/`vat_business_record_id`，HTTP 200 且 `succeeded=true` → 注册状态 2 成功 / 否则 3 失败，成功才写库，`submit_backup_data` 存完整原始请求 JSON），流程最后一步推送 Redis 队列（`factory:meiou:queue:VatRegOver`，`{Id: source_record_id}`；need_eori=0 → step2、=1 → step3，step1 只写库不推队列），不调用通知接口——两套流程无回退、无兼容。**

注册结果以向税局提交成功为判定标准：到达 step1 必已拿到 VRS 回执编号，`registration_status` 恒置 2（成功），step1 只发 200 成功通知。通知分**两轮**：

| 轮次 | 时机 | 脚本 | `data.receiptType` |
| ---- | ---- | ---- | ---- |
| 1 | 注册完成保存结果 | `rpa_step1_save.py` | `REGISTER_INFO`（注册信息） |
| 2 | 绑定 MTD 后（`need_eori=0`）**或**申请 EORI 后（`need_eori=1`） | `rpa_step2_mtdbind_save.py` / `rpa_step3_eori_save.py` | `ISSUED_INFO`（下号信息） |

> `need_eori=0` 时轮2 由 step2 发送，`need_eori=1` 时由 step3 发送（step2 不发，等待 EORI 完成后 step3 发）。

| 项目 | 值 |
| ---- | ---- |
| 地址 | `POST https://test-cloud.usaeu.com/prod-api/delivery/rpa/autoRegisterCallback`；优先级：受理请求 `bizParam.callback_url` > RPA runner 传参 > 默认地址 |
| 信封 | `{code, msg, ProcessMode:"async", data, bizParam}`；`bizParam` 为受理时原样落库（`biz_param` 列）回传 |
| 轮1 成功 | `{code:200, msg:"success", data:{receiptType:"REGISTER_INFO", vrsReceiptCode, mtdAccount, mtdPassword, mtdSecretKey, mtdRegisterEmail, files:[...]}, bizParam}`；`vrsReceiptCode` 即注册回执编号（step1 `reg_receipt_number` 参数），`mtd*` 为落库值，`mtdRegisterEmail` 取 `uk_vat_register.customer_email` 列；`files` 每项 `{url, name, type}`，`type` 枚举：注册回执文件 → `REGISTRATION_RECEIPT_FILE`、注册确认文件 → `REGISTRATION_CONFIRMATION_FILE`，`url` 为 OSS 相对路径（不带域名、不签名，对象键 `common-test/generatefile/{年}/uk_vat_register/{record_id}/{文件名}`，record_id 为 `uk_vat_register` 行主键子目录——文件名是 HMRC 页面标题固定名，子目录隔离不同记录防同名互盖；API 行结果文件上传至 SaaS 共享桶 `usaeu-1259285998`，域名由接收方拼接）；两者皆无时 `files=null` |
| 轮2 成功 | `{code:200, msg:"success", data:{receiptType:"ISSUED_INFO", vatNumber, declarationDeadline, firstDeclarationPeriodStart, firstDeclarationPeriodEnd, vatEffectiveDate, eoriNumber, files:[...]}, bizParam}`；`eoriNumber` 固定为 `GB + vatNumber + 000` 或 RPA 侧显式传入；其余字段取自 `uk_vat_register` 列：`vat_number`/`declaration_deadline`/`declaration_start_date`/`declaration_end_date`/`vatnumber_registration_date`；`files` `type` 枚举：VAT税号证书 → `VAT_CERTIFICATE_FILE`、申请EORI结果截图 → `EORI_APPLICATION_RESULT_SCREENSHOT`，`url` 同为 OSS 相对路径 |
| 失败 | `{code:500, msg:"failed", data:null, bizParam}` —— rpa_step1_get 重试达上限（status=4）；**非 200 通知 `data` 统一为 `null`** |
| 投递 | 10s 超时、2xx 视为送达；网络错误/超时/5xx 退避重试（最多 3 次：首次 + 2 次重试，间隔 2s/6s），4xx（408/429 瞬时状态除外）视为永久失败不重试；通知失败不影响主流程。每次通知的请求参数与投递结果写入 `uk_vat_register` 的 **`Notify1*` 5 列（轮1）/ `Notify2*` 5 列（轮2），每轮一套、互不覆盖**，并继续写入 `delivery_notify_log`（JSON，迁移见 `sql/add_notify_fields.sql` 与 `sql/add_delivery_notify_fields.sql`，旧库未迁移时仅告警不中断） |

## 部署

> **安全要求**：`api.php` 顶部 `API_KEY` 非空时强制 `X-API-Key` 鉴权（留空关闭，公网暴露务必配置密钥）；仍建议在 Web 服务器层增加内网网段或 IP 白名单，只映射 `api.php`；`config/`（含数据库密码）、`bin/`、`test/`、`sql/`、`logs/`（含请求日志）均不得经 Web 访问。

- **Apache**（仅映射 api.php，其余路径 403）：
  ```apache
  <Directory "/path/to/uk_vat_reg">
      Require ip 192.168.0.0/16            # 按实际内网网段调整
      <Files "api.php">
          Require all granted
      </Files>
      <FilesMatch "^(?!api\.php$).*">
          Require all denied
      </FilesMatch>
  </Directory>
  ```
- **Nginx**（仅代理 api.php，其余 404）：
  ```nginx
  location = /api.php {
      allow 192.168.0.0/16;                # 按实际内网网段调整
      deny all;
      include fastcgi_params;
      fastcgi_param SCRIPT_FILENAME /path/to/uk_vat_reg/api.php;
      fastcgi_pass unix:/run/php/php8.3-fpm.sock;
  }
  location / { return 404; }
  ```
- **本地调试**：`php -S 127.0.0.1:8099 api.php`（PHP 内置服务器路由模式，仅本机回环）。

纵深防御：`bin/sync.php` 已内置 `PHP_SAPI === 'cli'` 守卫，经 HTTP 调用返回 403；请勿将仓库其余脚本（`test/*.php` 等）放入 Web 可执行路径。

## 测试

```bash
# 纯逻辑测试（国家映射 / 转换工具 / 校验器，无 DB 写入）
php test/test-api-validator.php

# 集成测试（直连 rpa_test 真实库：信封、聚合错误、INSERT/UPDATE/skip，测试数据自动清理）
php test/test-api.php
```

> test 环境下 `test/test-api.php` 不新建邮箱，主用例与无 BusinessId 降级用例均固定复用 `registrationvat19413@usaeu.com`（清理只删 DB 行，不动邮箱服务器）。prod 环境运行集成测试会真实创建 `registrationvat{N}@usaeu.com` 邮箱，请勿随意直跑。

## 工具脚本

- `bin/align-email-sequence.php` — 一次性运维脚本：test 库 `email_sequence_manager` 序号与共享邮件服务器高水位脱节时（建邮箱逐个重试、耗时骤增），探测并校准序号表。
