# 法国 LEKO 下号（UIN）API 流程 RPA 接口

> 面向 **API 流程专用 RPA**（独立于 source 流程 RPA）。source 流程 RPA 直连 source 库
> 认领 `EPRRegInfo.PushTaxBureauStatus=3`；API 流程没有 source 库记录，改走本组接口：
> **取数只返回 API 流程注册成功的数据**（`data_source='api'` 且 `status=2`），两套队列零交集。

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

完整地址：
- 取数：`http://automation.usaeu.com:8888/fr_epr_reg/api/epr/leko-rpa/claim`
- 回执：`http://automation.usaeu.com:8888/fr_epr_reg/api/epr/leko-rpa/result`

访问控制：`internal.network`（172.16.0.0/16，与旧 `/api/epr/*` 一致）。

## 流程总览（与 source 流程三步一一对应）

| 步骤 | source 流程（`python/rpa_leko_step*.py`，不变） | API 流程（新增） |
|---|---|---|
| 取任务 | 直连 source 库 SELECT（`PushTaxBureauStatus=3`）+ `ModificationDate` 作锁 | `POST /api/epr/leko-rpa/claim`（`rpa_claimed_at` 作锁） |
| 传证书 | `upload_to_tencent_cos(...)` 传 vat 桶，返回完整 URL | `python/rpa_leko_api_step2_upload_oss.py`：固定 `usaeu-1259285998` + `{OSS_API_PREFIX}{年}/fr_epr_reg/leko/{BSN}/{文件名}`，返回**相对 key** |
| 回写 | `Base_AnnexesFile` + `EPRRegInfo.RegBackNumber` + `PushTaxBureauStatus=4` | `POST /api/epr/leko-rpa/result`：写 `fr_epr_reg.reg_code / cert_oss_url`，**由服务端**调用 SaaS 异步通知 |

任务来源：`/api/epr/fr/registration-mail` 发信成功后，按 POA **每公司一行**幂等写入
`fr_epr_reg`（`data_source='api'`；BSN 从 POA key 的 `leko/{BSN}/` 段解析、名称从
`POA-{净化NameEng}.pdf` 反解；`biz_param` 存受理 bizParam 原样 JSON）。

## 状态机（`fr_epr_reg.rpa_status`）

| 值 | 含义 | 迁移 |
|---|---|---|
| 0 | 待认领 | 落库时写入；重复提交（未完成）重置为 0 |
| 1 | 处理中 | claim 原子置 1、`rpa_attempts+1`、`rpa_claimed_at=now` |
| 2 | 完成 | result success：写 `reg_code`/`cert_oss_url` |
| 3 | 失败 | result failed，或认领超限且过期（服务端发失败通知） |

- 认领条件：`data_source='api' AND status=2 AND rpa_attempts < FR_RPA_CLAIM_MAX_ATTEMPTS(3)
  AND (rpa_status=0 OR (rpa_status=1 AND rpa_claimed_at < now - FR_RPA_CLAIM_STALE_MINUTES(120)))`
  —— 已认领但 RPA 崩溃的任务在过期后可被重新认领。
- 已完成（2）的任务回执幂等：返回 `already completed`，**不重复通知** SaaS。

## POST /api/epr/leko-rpa/claim

无请求体。认领一条任务并返回（字段名与 source 流程 step1 对齐）：

```json
{
    "code": 200,
    "msg": "success",
    "data": {
        "task_id": 42,
        "attempts": 1,
        "BusinessSerialNumber": "FREPR2026000001",
        "NameEng": "EXAMPLE COMPANY SAS",
        "xlsx": {"key": "common-test/generatefile/2026/fr_epr_reg/merged/merged_....xlsx",
                 "name": "merged_....xlsx", "signed_url": "https://usaeu-1259285998.cos...?sign=..."},
        "pdf":  {"key": "common-test/generatefile/2026/fr_epr_reg/leko/FREPR2026000001/POA-....pdf",
                 "name": "POA-....pdf", "signed_url": "https://usaeu-1259285998.cos...?sign=..."},
        "bizParam": {"BusinessSerialNumber": "FREPR2026000001", "callback_url": "https://..."}
    }
}
```

- 无待办任务：`{"code":200,"msg":"no task available","data":null}`。
- `signed_url`：服务端生成的**临时下载 URL**（默认 10 分钟，`FR_RPA_SIGNED_URL_TTL_MINUTES`），
  RPA 无需持有 COS 凭证；**签名 URL 仅 GET 可用**（探测文件用 `GET + Range`，勿用 HEAD）。
- API 流程没有 source 库 ID 类字段（`RegisterID`/`EPRRegInfoID`/`F_Mobile` 等）；等价标识由受理
  `bizParam` 承载并原样透传。
- 认领流程会先处理"认领超限且过期"的任务（置失败并发送失败通知）。

## POST /api/epr/leko-rpa/result

```json
{"task_id": 42, "status": "success", "reg_code": "UIN123456",
 "cert_key": "common-test/generatefile/2026/fr_epr_reg/leko/FREPR2026000001/UIN-Certificate.pdf",
 "cert_name": "UIN-Certificate.pdf"}
```

- `status=success`：`reg_code` 必填；`cert_key` 选填，**必须落在本任务 `leko/{BusinessSerialNumber}/`
  目录内**（防串任务）；服务端写 `reg_code`/`cert_oss_url` 后调用 SaaS 异步通知。
- `status=failed`：`error_message` 必填（缺省 `UIN retrieval failed`）；置 `rpa_status=3` 并发失败通知。
- 响应：`{"code":200,"msg":"success","data":{"task_id":42,"notify_status":"success|failed"}}`；
  `notify_status` 为服务端向 SaaS 的投递结果（SaaS 侧通知失败时服务端已记录 `notify_*` 列，可人工补发）。
- 错误：`400`（入参/证书 key 校验失败）、`404`（任务不存在）、`409`（任务未处于认领中）。

## SaaS 异步通知（契约 §13 delivery/rpa/callback）

由**服务端**在回执成功后发送（RPA 不直连 SaaS）：

- 地址：受理 `bizParam.callback_url` / `callbackUrl` 优先，缺省 `FR_RPA_DELIVERY_CALLBACK_URL`
  （默认 `https://test-cloud.usaeu.com/prod-api/delivery/rpa/callback`）。
- 重试：最多 `FR_RPA_DELIVERY_MAX_ATTEMPTS`(3) 次、退避 2s/6s；2xx 成功；4xx（除 408/429）不重试。
- 成功载荷：`receiptType=ISSUED_INFO`、`uin=<reg_code>`、`files=[{"url": 证书相对key, "name": …, "type": "UIN_CERTIFICATE_FILE"}]`；
  失败载荷：`code=500`、`data=null`、错误原因放 `msg`；`bizParam` 均原样回传。
- 投递结果写 `notify_request/notify_response/notify_status/notify_attempts/notify_time`。

> 契约登记情况（2026-09-12）：`UIN_CERTIFICATE_FILE` 已入 §5.2 文件类型枚举，载荷（`uin` 字段）见 §14.2.3；SaaS 侧实际接受情况待联调确认。

## RPA 脚本（`python/`，自包含、不互相 import）

| 脚本 | 作用 |
|---|---|
| `rpa_leko_api_step1_get.py` | 调 claim 认领任务（返回 dict/None） |
| `rpa_leko_api_step1_search_error_sendmsg.py` | **查询异常告警**：门户查询注册码检测到多家公司时发企业微信群消息（文案与 source 流程一致） |
| `rpa_leko_api_step1_search_error_update_task.py` | **查询异常回传**：调 result（status=failed）→ 服务端置 `rpa_status=3` 并发送 SaaS 失败通知 |
| `rpa_leko_api_step2_upload_oss.py` | 证书上传 usaeu（`.../fr_epr_reg/leko/{BSN}/`），返回相对 key |
| `rpa_leko_api_step3_save.py` | 调 result 回执成功/失败（服务端负责 SaaS 通知） |

### 错误处理对照（source 流程 → API 流程）

| 异常场景 | source 流程 | API 流程 |
|---|---|---|
| 门户查询**检测到多家公司**（歧义） | `rpa_leko_step1_search_error_sendmsg.py` 企业微信告警 + `rpa_leko_step1_search_error_update_reginfo.py` 写 `EPRRegInfo.PushTaxBureauStatus`/`Remarks` | `rpa_leko_api_step1_search_error_sendmsg.py` 企业微信告警（同文案）+ `rpa_leko_api_step1_search_error_update_task.py` 调 `/result`（failed）→ 服务端置 `rpa_status=3` + SaaS 失败通知（§13，code=500） |
| 证书上传失败 | `rpa_leko_step2_upload_oss.py` 返回空字符串（调用方处理） | `rpa_leko_api_step2_upload_oss.py` 抛 `RuntimeError`（调用方决定重试/回传失败） |
| 下号业务失败 | step3 未能完成（人工介入） | `rpa_leko_api_step3_save.py::report_failure()` 调 `/result`（failed） |
| **认领重试超限** | source 流程按 `ModificationDate` 租约 + 人工处理 | **服务端自动处理**：claim 时清理"超限且过期"任务 → 置 `rpa_status=3` 并发送 SaaS 失败通知（RPA 侧无需额外脚本） |
| 无待办任务 | step1 打印"没有查询到符合条件的记录" | claim 返回 `data:null`（step1 打印"没有待处理的下号任务"） |

部署常量在各脚本文件头（服务地址 / `API_FLOW_OSS_PREFIX` 等），需与服务端 `.env`
（`OSS_API_PREFIX`、`OSS_API_MODULE_DIR`）及环境（test=common-test、prod=common-prod）一致。

## 联调步骤（rpa_test）

1. 在 `rpa_test` 库执行 `database/rpa_test_schema.sql`（含 1b 增量段）。
2. 模拟 SaaS 调 `/api/epr/fr/registration-mail`（测试可用 fake OSS/SMTP）→ 检查 `fr_epr_reg`
   出现 `data_source='api'`、`rpa_status=0` 的公司行。
3. `curl -X POST .../api/epr/leko-rpa/claim` → 返回任务与签名 URL；用 `curl -r 0-0` 验证签名 URL 可下载。
4. 上传证书后 `curl -X POST .../api/epr/leko-rpa/result`（success + reg_code + cert_key）→
   响应 `notify_status=success`；检查 `reg_code`/`cert_oss_url`/`notify_*` 列与 SaaS 侧收到通知。
5. 重复第 4 步 → `already completed`（幂等、不重复通知）。

## 不影响 source 流程

- source 流程行（`data_source='source'`，含历史存量）不被本组接口读取/修改；`TargetRepository`
  与 source 库回写路径零改动；`python/rpa_leko_step*.py` 不变。
- 新列全部可空/有默认；`data_source` 存量行回填 `'source'`。
