# CDS 数据对接新系统接口文档（GB_VAT_REGISTER_CDS_FILE）

## 概述

公司新老两个系统并行期：老系统（`DataSource='source'`，走 `api_import.php` / `api_search.php`）与新系统（`DataSource='api'`）管理同一批 CDS 账号。账号在 `cds_download` 表一行全局唯一（按 `account_id`），归属可交接（source → api 单向），RPA 统一处理两块账号，新系统账号的文件下载结果通过 delivery 平台统一异步结果通知接口回调。

统一接口契约沿用 app_withdrawn 仓库 `docs/UNIFIED_API_DESIGN.md`（§13 异步结果回调接口）。

## 1. 接收接口

| 项目 | 值 |
| ---- | ---- |
| **接口名称** | CDS 账号接收接口（新系统） |
| **请求方式** | POST（`Content-Type: application/json`） |
| **入口文件** | `api/api_cds_register.php` |
| **测试地址** | `http://rpa.test.fix.usaeu.com/uk_cds/api/api_cds_register.php` |
| **生产地址** | `http://rpa.fix.usaeu.com/uk_cds/api/api_cds_register.php` |
| **鉴权** | 接口顶部 `API_KEY` 非空时校验 `X-API-Key` 请求头（缺失/不匹配返回 401）；留空则关闭鉴权 |
| **受理模式** | `async`：受理即返回，文件下载完成后另行回调 |

### 1.1 请求参数

| 参数 | 类型 | 必填 | 说明 |
| ---- | ---- | ---- | ---- |
| `PushType` | string | 是 | 固定 `GB_VAT_REGISTER_CDS_FILE`（大小写不敏感） |
| `Country` | string | 是 | 固定 `GB`（大小写不敏感） |
| `bizParam` | object | 是 | 业务标识信息，**原样回传**（通知时作为 `bizParam` 原样带回，是新系统定位内部数据的依据，见 §3.1） |
| `Data` | object | 是 | 业务数据（见下表） |

**bizParam 字段：**

| 字段 | 类型 | 必填 | 说明 |
| ---- | ---- | ---- | ---- |
| `BusinessSerialNumber` | string | 是 | 业务流水号（≤40 字符）；随 `bizParam` 原样落库并在通知时回传 |
| `BusinessId` | string/number | 建议 | 业务 ID，原样回传 |
| 其他字段 | — | 否 | 任意附加字段，原样回传 |

> **定位约定**：文件通知按受理请求的 `bizParam` **原样回传**——新系统要用来定位内部数据的字段（流水号/业务 ID 等）请在推送时放入 `bizParam`，本接口不做校验、不改名、不丢字段。
> **重推语义**：同一账号重复推送（新流水号）时 `bizParam` 以**最新一次推送为准**；此后该账号的文件通知回传最新 `bizParam`，旧推送单不再收到通知。

**Data 字段（CDS 账号凭证，字段名 PascalCase 对齐 UNIFIED_API_DESIGN §4 规范）：**

| 字段 | 类型 | 必填 | 长度 | 说明 |
| ---- | ---- | ---- | ---- | ---- |
| `AccountId` | string | 是 | ≤50 | CDS 账号 ID（全局唯一，接管/幂等匹配键） |
| `Password` | string | 是 | ≤100 | CDS 密码 |
| `AppKey` | string | 是 | ≤100 | 平台 KEY |
| `AccountAlias` | string | 否 | ≤50 | 账号别名 = 对接系统内本条数据的唯一标识（与老系统 `api_import` 流程同语义；落库后随通知 `data.account_alias` 带回，可不传） |

**请求示例：**

请求体即本目录 `英国CDS账号.json` 全文（仿 app_withdrawn `docs/英国VAT注册.json` 惯例）：

```json
{
    "PushType": "GB_VAT_REGISTER_CDS_FILE",
    "Country": "GB",
    "bizParam": {
        "BusinessSerialNumber": "CDS20260903000001",
        "BusinessId": 123456
    },
    "Data": {
        "AccountId": "test001",
        "Password": "password123",
        "AppKey": "appkey456789"
    }
}
```

### 1.2 响应格式

**受理成功（async）：**

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

**失败响应：** `code=400`（校验错误分号拼接）/ `code=405`（非 POST）/ `code=401`（鉴权失败），`data=null`，`bizParam` 原样回传（有则回）。

| code | 含义 |
| ---- | ---- |
| 200 | 受理成功（处理结果经通知接口另行送达） |
| 400 | 参数/业务校验失败（`msg` 为分号拼接的错误原因） |
| 401 | `API_KEY` 非空时 `X-API-Key` 缺失或不匹配 |
| 405 | 非 POST 请求 |

### 1.3 幂等与归属语义

账号在 `cds_download` 一行全局唯一（按 `account_id`），处理规则：

| 场景 | 行为 |
| ---- | ---- |
| `account_id` 不存在 | INSERT：`DataSource='api'`，初始执行状态与 `api_import.php` 硬编码一致（`last_execute_result=0`、`last_file_date='202001'`、`last_execute_year_month_day='20250101'`） |
| 存在且 `DataSource='source'` | **接管**：归属改 `api` + 更新凭证/bizParam；**不重置执行进度**（`last_execute_*` 列不动） |
| 存在且 `DataSource='api'` | 幂等更新：更新凭证/bizParam（同单重推或新单接管均更新） |

> 反向不成立：老系统 `api_import.php` 的先删后插仅限 `DataSource='source'` 行，不抢回已交接的新系统账号。
> 同一账号并发推送由 `sp_getapplock`（资源名 `cds_download_gb_upsert`）串行处理。

## 2. RPA 流程

- `python/rpa_cds_get.py`：**不区分系统**，按执行状态统一领取（两系统账号一视同仁）。
- `python/rpa_cds_save.py`：保存文件时按账号**当前归属**分流：
  - 归属 `api` 行：文件解析调用新系统解析接口（`NEW_FILE_ANALYSIS_API`，传参 `FileUrl`+`FileName`、返回 `{succeeded, data:{TotalPostponed}}` 与老接口完全一致；**当前配置为老接口地址，新系统接口地址提供后替换脚本顶部常量**），插入日志后发异步结果通知（§3）；
  - 归属 `source` 行：走老接口解析、**不通知**（老系统经 `api_search.php` 自助查询）。

## 3. 异步结果通知接口（文件下载完成回调）

> 模式同 UNIFIED_API_DESIGN.md §13（delivery/rpa/callback）：RPA 保存成功（文件上传 COS + 金额解析完成 + 日志落库）后回调，**成功才通知**；失败由 RPA 重试兜底（`last_execute_result=3` 重试机制）。

### 3.1 回调数据

**成功回调（`code=200`）：**

```json
{
  "code": 200,
  "msg": "success",
  "ProcessMode": "async",
  "data": {
    "task_id": 123,
    "account_id": "test001",
    "account_alias": "",
    "file_date": "202608",
    "file_type": "PVA",
    "total_postponed": 1234.56,
    "files": [
      {
        "url": "https://usaeu-1259285998.cos.ap-guangzhou.myqcloud.com/common/PVA/test001/xxx.pdf",
        "name": "xxx.pdf",
        "type": "CDS_PVA_FILE"
      }
    ]
  },
  "bizParam": {
    "BusinessSerialNumber": "CDS20260903000001",
    "BusinessId": 123456
  }
}
```

| 字段 | 类型 | 说明 |
| ---- | ---- | ---- |
| `code` | int | `200`=成功 |
| `msg` | string | 成功 `success` |
| `ProcessMode` | string | `async` |
| `data.task_id` | int | `cds_download_log.id`（幂等键之一） |
| `data.account_id` / `data.account_alias` | string | 账号标识（新系统行 alias 可为空串） |
| `data.file_date` | string | 文件对应时间（YYYYMM，6 位） |
| `data.file_type` | string | `PVA` / `C79` |
| `data.total_postponed` | number | 递延金额（解析接口返回） |
| `data.files` | array | 文件数组（1 行即 1 个元素）；`url`=腾讯云 COS **完整 URL**（可直接下载）、`name`=文件名、`type` 枚举见下 |
| `bizParam` | object | 受理请求**原样回传** |

> **新系统定位方式**：`bizParam` 回传受理请求原样内容（定位内部数据的依据，见 §1.1 定位约定），`data.account_id` + `file_date` + `file_type` 标识具体账号与文件（账号行全局唯一）。`data` 字段命名沿用 §13 `task_id` 风格。同一通知可能重复送达，按 `bizParam` + `task_id` 幂等（覆盖式更新，以最后为准）。

**`data.files[].type` 文件类型枚举（CDS 专用，勿自定义）：**

| 枚举 | 说明 |
| ---- | ---- |
| `CDS_PVA_FILE` | PVA 文件 |
| `CDS_C79_FILE` | C79 文件 |

### 3.2 投递规则

| 项目 | 值 |
| ---- | ---- |
| 地址优先级 | RPA runner 传参 `delivery_callback_url` > 默认 `https://test-cloud.usaeu.com/prod-api/delivery/rpa/callback`（生产部署时以 runner 传参指向生产地址） |
| 超时 | 10 秒 |
| 重试 | 2xx 视为送达；网络错误/超时/5xx 退避重试（最多 3 次：首次 + 2 次重试，间隔 2s/6s）；4xx（408/429 瞬时状态除外）视为永久失败不重试 |
| 幂等 | 重复通知可能发生，接收端按 `bizParam` + `task_id` 幂等处理（覆盖式更新，以最后为准） |
| 失败影响 | 通知失败**不影响主流程**（文件已落库，由 RPA 重试机制兜底） |

### 3.3 通知日志表

每次通知的请求参数与投递结果写入 `cds_download_notify_log`（表不存在/写入失败仅告警，不影响主流程）：

| 列 | 类型 | 说明 |
| ---- | ---- | ---- |
| `id` | int PK | 自增 |
| `log_id` | int | 关联 `cds_download_log.id`（=`task_id`） |
| `account_id` / `account_alias` | string | 账号标识 |
| `notify_url` | string | 通知地址 |
| `request_payload` | string | 通知请求 JSON |
| `response_text` | string | 响应内容/错误 |
| `http_code` | int | 最后一次 HTTP 状态码 |
| `attempts` | int | 尝试次数 |
| `status` | string | `success` / `failed` |
| `created_time` | datetime | 通知时间（默认 GETDATE()） |

## 4. 部署顺序（硬性）

1. 在正式库 `rpa` 执行 `database/add_cds_data_source_fields.sql`（幂等、事务化：`cds_download` 加 `DataSource`/`biz_param` 列 + CHECK/默认约束；新建 `cds_download_notify_log` 表）；
2. 部署/更新 `api/api_cds_register.php`、`api/api_import.php`（新代码已引用新列，旧库直接运行会报错）；
3. 更新 `python/rpa_cds_save.py`（新系统解析接口地址确认后替换 `NEW_FILE_ANALYSIS_API` 常量）；
4. app_withdrawn `config/downstream.php` 增加转发（其 `docs/UNIFIED_API_DESIGN.md` 的 §4.4.18 请求实例 / §5.2 文件类型枚举 / §9 映射表已登记完成）：
   ```php
   'GB_VAT_REGISTER_CDS_FILE' => 'http://rpa.test.fix.usaeu.com/uk_cds/api/api_cds_register.php',
   ```

## 5. 边界说明

- 账号被新系统接收时，若该账号本月文件**已存在**（老系统时期下载过同月同类型文件），RPA 不会重复下载，该文件也不会补发通知——新系统从下一次文件开始接收数据；
- 仅 `account_alias` 无 `account_id` 的存量老行不在本接口匹配范围（保持 source 不动）；
- `api_search.php` 不受影响：老系统可查询全部文件记录（含已交接新系统的账号），懒加载回写 `total_postponed` 行为保持不变。
