# be_epr_pack_register.php 开发文档

## 概述

**文件路径**: `job/be_epr_pack_register.php`

**功能**: 比利时包装法注册自动化任务（阶段一）。

从 SaaS 库（`vat_db`）查询符合条件的比利时包装法注册数据，获取已生成的文件（授权书 Mandat + 会员合同 Membership），将公司信息和文件路径写入本地库（`rpa`）的 `pack_be_register` 表，等待后续推送服务处理。处理完成后更新 SaaS 端状态为"待推送"。

**触发方式**: CLI 脚本，以服务模式启动，每 3 分钟执行一个周期（当前 `sleep(60)` 被注释，每次请求只执行一轮）。

```bash
php job/be_epr_pack_register.php
```

---

## 架构设计

```
                        ┌──────────────────────┐
                        │    SaaS 系统 (vat_db) │
                        │                       │
                        │  EPRBusinessRecord    │
                        │  EPRRegInfo           │
                        │  Base_Customer_Company│
                        │  GeneralTemplateEPR   │
                        │  Base_AnnexesFile     │
                        │  RecyclingType        │
                        │  ServiceItems         │
                        │  Country              │
                        └──────┬───────────────┘
                               │ SQL JOIN 查询
                               ▼
┌──────────────────────────────────────────────────────┐
│              be_epr_pack_register.php                 │
│                                                       │
│  processBePackData()                                  │
│   ├─ validateData()         ← 字段校验               │
│   ├─ getFilePathsFromAnnexes() ← 查 Base_AnnexesFile │
│   ├─ insertOrUpdateRegistrationInfo()                 │
│   │    └─ INSERT/UPDATE pack_be_register             │
│   └─ updateEPRRegInfoStatus() ← 更新 SaaS 端状态     │
└───────────────────────┬──────────────────────────────┘
                        │
                        ▼
┌──────────────────────────────────────────────────────┐
│              本地库 (rpa)                             │
│                                                       │
│  pack_be_register                                     │
│   ├─ status=0 待推送                                  │
│   ├─ status=1 推送中                                  │
│   └─ status=2 推送完成                                │
└──────────────────────────────────────────────────────┘
```

---

## 类结构与方法说明

### 类: `be_epr_pack_register`

| 属性 | 类型 | 说明 |
|---|---|---|
| `$sqlServerDb` | `SqlServerDb` | SaaS 库连接（`vat_db`） |
| `$localsqlServerDb` | `SqlServerDb` | 本地业务库连接（`rpa`） |
| `$downloadBaseDir` | `string` | 本地文件下载根目录 `resources/`（当前未使用） |

---

### 1. `__construct()` (L56)

初始化 `downloadBaseDir` 路径。

---

### 2. `run()` (L63) — 主入口

```
run()
  ├─ initializeDatabaseConnections()    // 连接 SaaS 库 + 本地库
  ├─ processBePackData()                // 查询并处理数据
  └─ closeDatabaseConnections()         // 关闭连接
```

---

### 3. `initializeDatabaseConnections()` (L81)

创建两个数据库连接：

| 连接 | 数据库 | 来源配置 |
|---|---|---|
| `$this->sqlServerDb` | `vat_db` | `getSqlServerConfig()` |
| `$this->localsqlServerDb` | `rpa` | `getlocalSqlserverConfig()` |

两者共用同一 SQL Server 实例 `gz-mssql-4jvvbair.sql.tencentcdb.com,25944`。

---

### 4. `processBePackData()` (L122) — 核心处理逻辑

**查询条件**（全部 AND）:

| 条件 | 字段 | 值 |
|---|---|---|
| 国家 | `br.Country` | `'BE'`（比利时） |
| 状态 | `ei.PushTaxBureauStatus` | `1`（待生成文件） |
| 推送类型 | `ei.PushType` | `'301'` |
| 回收类型 | `rt.TypeName` | `'包装法'` |
| 服务项 | `si.ServiceItemName` | `'包装法注册'` |
| 限制数量 | `TOP 10` | 每次取 10 条 |

**关联表**:

```
EPRBusinessRecord AS br
  LEFT JOIN EPRRegInfo AS ei               ON br.ID = ei.EPRBusinessRecordId
  LEFT JOIN GeneralTemplateEPR AS gt        ON gt.ID = br.ID
  LEFT JOIN Base_Customer_Company AS bc     ON bc.ID = br.CompanyId
  LEFT JOIN RecyclingType AS rt            ON rt.ID = br.RecyclingTypeId
  LEFT JOIN Country AS c                   ON c.CountryName = bc.Country
  LEFT JOIN ServiceItems AS si             ON br.ServiceItemId = si.ID
```

**单条记录处理流程**:

```
processBePackData() 对每条记录执行:
  │
  ├─ Step 1: 查本地库是否已存在（pack_be_register WHERE tid=?）
  │    ├─ status=2（已推送） → 跳过，更新 SaaS 为 status=-1
  │    └─ 其他 → 继续处理（会做 UPDATE）
  │
  ├─ Step 2: validateData() 数据校验
  │    ├─ 失败 → updateEPRRegInfoStatus(-1, errorMsg) → continue
  │    └─ 通过 → 继续
  │
  ├─ Step 3: getFilePathsFromAnnexes() 查文件路径
  │    ├─ 文件路径为空 → updateEPRRegInfoStatus(-1, errorMsg) → continue
  │    └─ 有文件 → 继续
  │
  ├─ Step 4: insertOrUpdateRegistrationInfo() 写入本地库
  │    ├─ 失败 → updateEPRRegInfoStatus(-1, errorMsg) → continue
  │    └─ 成功 → 继续
  │
  └─ Step 5: updateEPRRegInfoStatus(2) 状态置为"待推送"
```

**异常处理**: 每条记录的 `try/catch` 捕获异常后更新 SaaS 端 `status=-1`，继续处理下一条，不中断整体流程。

---

### 5. `getFilePathsFromAnnexes($eprRegInfoId)` (L215) — 文件路径查询

查询 `Base_AnnexesFile` 表，通过 `InfoId = EPRRegInfoID` 关联。

**文件识别逻辑**（按文件名关键字）:

| 文件类型 | 匹配关键字 |
|---|---|
| 授权书 (Mandat) | `mandat`, `poa`, `vollmacht` |
| 会员合同 (Membership) | `membership`, `fostplus`, `onboarding`, `contract`(含 `be_` 前缀) |

**路径替换**: 本地物理路径 `MAIN_SITE_PATH` → 可访问 URL `FILE_SITE_PATH`

```php
// production 环境:
// G:/fileAnnexes/xxx → https://file.usaeu.com/xxx
// test 环境:
// G:/fileAnnexesTest/xxx → http://testfile.usaeu.com/xxx
```

详见 [config/api_config.php](file:///d:/phpstudy_pro/WWW/vat_api_se_client/config/api_config.php#L30-L37)。

---

### 6. `validateData($dataRow)` (L277) — 数据校验

| 校验项 | 规则 | 失败时返回 |
|---|---|---|
| 必填字段 12 个 | 非空 `trim()` | `SaaS 数据字段缺失: ...` |
| 公司地址 | `CompanyAddressLine1En` 至少一个非空 | 同上 |
| 公司注册号/增值税号 | `RegNumber` 或 `VATNumber` 至少一个非空 | 同上 |
| 公司名称长度 | `mb_strlen() <= 60` | `公司名称长度超限` |
| 法人邮箱 | `FILTER_VALIDATE_EMAIL` | `法人邮箱格式非法` |
| 法人电话 | `preg_replace('[+\-\s]')` 后 >= 5 位数字 | `法人电话格式非法` |
| 法人拼音姓名 | 含空格分隔（"名 姓"格式） | `法人拼音姓名未按"名 姓"空格分隔` |

**必填字段清单**:

- `BusinessSerialNumber`（业务流水号）
- `RegisterID`（业务记录ID）
- `EPRRegInfoID`（注册信息ID）
- `NameEng`（公司英文名）
- `LegalPersonFullNamePinYin`（法人拼音姓名）
- `LegalPersonGender`（法人性别）
- `LegalPersonPhone`（法人电话）
- `LegalPersonEmail`（法人邮箱）
- `CompanyAddressPostcode`（邮编）
- `CityEngName`（城市）
- `ProductsRange_En`（产品范围）
- `Country`（国家）

---

### 7. `insertOrUpdateRegistrationInfo($dataRow, $mandatPath, $membershipPath, $existing)` (L370) — 写入本地库

**数据映射**:

| pack_be_register 字段 | 来源 | 处理逻辑 |
|---|---|---|
| `tid` | `BusinessSerialNumber` | 唯一标识 |
| `VATBusinessID` | `RegisterID` | EPRBusinessRecord.ID |
| `EPRRegInfoID` | `EPRRegInfoID` | EPRRegInfo.ID |
| `VAT_or_company_number` | | 非欧盟: `CountryTwoCode + RegNumber`; 欧盟: `VATNumber` |
| `Company_Name_en` | `NameEng` | `mb_substr(0, 60)` 截断 |
| `Company_Name_cn` | `NameCN` | 公司中文名 |
| `StreetNrBox` | `CompanyAddressLine1En` | 公司地址 |
| `Zipcode` | `CompanyAddressPostcode` | 邮编 |
| `City` | `CityEngName` | 城市 |
| `Country` | `CountryName_en` | 国家英文名 |
| `Phone_number` | `LegalPersonPhone` | 去除 `+`, `-`, 空格 |
| `VAT_nr` | | 同 `VAT_or_company_number` |
| `Legal_LastName` | `LegalPersonFullNamePinYin` | 通过 `parseLegalPersonName()` 解析 |
| `Legal_FirstName` | `LegalPersonFullNamePinYin` | 通过 `parseLegalPersonName()` 解析 |
| `Legal_Salutation` | `LegalPersonGender` | 1→Mr, 其他→Mrs |
| `Legal_Email` | `LegalPersonEmail` | |
| `Sector` | `ProductsRange_En` | 销售产品类型 |
| `Power_of_attorney` | `mandatPath` | 授权书文件 URL |
| `Membership_agreement` | `membershipPath` | 会员合同文件 URL |
| `status` | | 固定 `0`（待推送） |
| `msg` | | 固定 `''` |

**逻辑**: 已有记录 → `UPDATE`；无记录 → `INSERT`。

**`parseLegalPersonName()`**

位于 [public/function.php](file:///d:/phpstudy_pro/WWW/vat_api_se_client/public/function.php#L44-L60):

```php
// "名 姓" → last_name=姓, first_name=名
// 无空格 → last_name=全名, first_name=''
```

---

### 8. `updateEPRRegInfoStatus($eprRegInfoId, $status, $message)` (L472) — 更新 SaaS 端状态

```sql
UPDATE EPRRegInfo SET PushTaxBureauStatus = :status WHERE ID = :id
```

**状态枚举**:

| 状态值 | 含义 | 设置时机 |
|---|---|---|
| `1` | 待生成文件 | 初始状态（SQL 查询条件） |
| `2` | 待推送 | `processBePackData` 写入本地库成功后 |
| `3` | 推送完成 | 由推送服务设置 |
| `-1` | 失败 | 校验不通过 / 文件获取失败 / 数据库写入失败 |

---

## 依赖关系

```
be_epr_pack_register.php
  ├─ vendor/autoload.php                     (composer)
  ├─ db/sql_server_db.php                    (SqlServerDb 类)
  ├─ config/database_config.php              (getSqlServerConfig, getlocalSqlserverConfig)
  ├─ config/api_config.php                   (MAIN_SITE_PATH, FILE_SITE_PATH)
  └─ public/function.php                     (logMessage, parseLegalPersonName, generateUUID)
```

### 引用的命名空间/类（声明但当前未使用）

```php
use PhpParser\Node\Stmt\TryCatch;           // 未使用
use setasign\Fpdi\Fpdi;                      // 未使用
use PhpOffice\PhpWord\IOFactory;             // 未使用
use Dompdf\Dompdf;                           // 未使用
```

这些 `use` 声明可能是从旧实现复制来的残留。

---

## 数据库表结构

### SaaS 库 (`vat_db`)

#### EPRBusinessRecord — 业务主记录

| 字段 | 说明 |
|---|---|
| `ID` | 主键 |
| `BusinessSerialNumber` | 业务流水号（如 `POEPR20260529000190`） |
| `CompanyId` | 关联 `Base_Customer_Company.ID` |
| `RecyclingTypeId` | 关联 `RecyclingType.ID` |
| `ServiceItemId` | 关联 `ServiceItems.ID` |
| `Country` | 国家二字码（如 `BE`） |

#### EPRRegInfo — 注册信息

| 字段 | 说明 |
|---|---|
| `ID` | 主键，作为 `Base_AnnexesFile.InfoId` 的关联键 |
| `EPRBusinessRecordId` | 关联 `EPRBusinessRecord.ID` |
| `PushTaxBureauStatus` | 状态: 1=待生成, 2=待推送, 3=已完成, -1=失败 |
| `PushType` | 推送类型（如 `301`） |

#### Base_Customer_Company — 客户公司

| 字段 | 说明 |
|---|---|
| `ID` | 主键 |
| `NameEng` | 公司英文名 |
| `NameCN` | 公司中文名 |
| `LegalPersonFullNamePinYin` | 法人拼音姓名 |
| `LegalPersonGender` | 法人性别 (1=男, 0=女) |
| `LegalPersonPhone` | 法人电话 |
| `LegalPersonEmail` | 法人邮箱 |
| `LegalPersonIDCardAddressEng` | 法人身份证地址 |
| `CompanyAddressPostcode` | 邮编 |
| `CityEngName` | 城市 |
| `Country` | 国家名 |
| `CountryTwoCode` | 国家二字码 |
| `RegNumber` | 公司注册号 |
| `VATNumber` | 增值税号 |
| `IsEUMember` | 是否欧盟成员 |

#### Base_AnnexesFile — 附件文件

| 字段 | 说明 |
|---|---|
| `InfoId` | 关联 `EPRRegInfo.ID` |
| `F_FilePath` | 文件路径 |
| `F_FileName` | 文件名 |
| `FileCategoryId` | 文件分类 GUID |

#### 其他关联表

| 表 | 关联字段 | 含义 |
|---|---|---|
| `GeneralTemplateEPR` | `ID → EPRBusinessRecord.ID` | 模板扩展信息 |
| `RecyclingType` | `ID → EPRBusinessRecord.RecyclingTypeId` | 回收类型（包装法等） |
| `ServiceItems` | `ID → EPRBusinessRecord.ServiceItemId` | 服务项（包装法注册等） |
| `Country` | `CountryName → Base_Customer_Company.Country` | 国家信息 |

### 本地库 (`rpa`)

#### pack_be_register — 包装法注册本地暂存

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | int | 自增主键 |
| `tid` | varchar | 业务流水号（唯一标识） |
| `VATBusinessID` | int | SaaS 业务记录 ID |
| `EPRRegInfoID` | int | SaaS 注册信息 ID |
| `VAT_or_company_number` | varchar | 增值税号/注册号 |
| `Company_Name_en` | varchar(60) | 公司英文名 |
| `Company_Name_cn` | varchar | 公司中文名 |
| `StreetNrBox` | varchar | 地址 |
| `Zipcode` | varchar | 邮编 |
| `City` | varchar | 城市 |
| `Country` | varchar | 国家 |
| `Phone_number` | varchar | 电话 |
| `VAT_nr` | varchar | 税号 |
| `Legal_LastName` | varchar | 法人姓 |
| `Legal_FirstName` | varchar | 法人名 |
| `Legal_Salutation` | varchar | 称呼（Mr/Mrs） |
| `Legal_Email` | varchar | 法人邮箱 |
| `Sector` | varchar | 产品范围 |
| `Power_of_attorney` | varchar | 授权书 URL |
| `Membership_agreement` | varchar | 会员合同 URL |
| `status` | int | 0=待推送, 1=推送中, 2=推送完成 |
| `msg` | varchar | 备注信息 |

---

## 执行流程时序图

```
加载模块
  │
  ├─ require autoload.php, sql_server_db.php, database_config.php, api_config.php, function.php
  │
  ├─ 实例化 be_epr_pack_register()
  └─ $register->run()
       │
       ├─ initializeDatabaseConnections()
       │    ├─ new SqlServerDb(vat_db)  → 连接 SaaS 库
       │    └─ new SqlServerDb(rpa)     → 连接本地库
       │
       ├─ processBePackData()
       │    ├─ SELECT TOP 10 WHERE Country='BE' AND PushTaxBureauStatus=1 AND PushType='301' ...
       │    │
       │    └─ foreach $results as $record:
       │         ├─ 查本地库: pack_be_register WHERE tid=?
       │         ├─ status=2 → 跳过 continue
       │         │
       │         ├─ validateData($record)
       │         │    └─ 字段缺失/格式不合法 → updateEPRRegInfoStatus(-1) → continue
       │         │
       │         ├─ getFilePathsFromAnnexes(EPRRegInfoID)
       │         │    ├─ SELECT F_FilePath, F_FileName FROM Base_AnnexesFile WHERE InfoId=?
       │         │    ├─ 路径替换 MAIN_SITE_PATH → FILE_SITE_PATH
       │         │    └─ 按文件名关键字识别 mandat / membership
       │         │
       │         ├─ insertOrUpdateRegistrationInfo(...)
       │         │    ├─ 解析法人姓名 → Legal_FirstName / Legal_LastName
       │         │    ├─ 欧盟/非欧盟 → VAT_or_company_number
       │         │    ├─ 组装 insertData
       │         │    ├─ 已有记录 → UPDATE pack_be_register
       │         │    └─ 新记录   → INSERT pack_be_register
       │         │
       │         └─ updateEPRRegInfoStatus(EPRRegInfoID, 2)
       │              └─ UPDATE EPRRegInfo SET PushTaxBureauStatus=2 WHERE ID=?
       │
       └─ closeDatabaseConnections()
```

---

## 配置说明

### `config/database_config.php`

| 配置函数 | 数据库 | 用途 |
|---|---|---|
| `getSqlServerConfig()` | `vat_db` | SaaS 系统主库 |
| `getlocalSqlserverConfig()` | `rpa` | 本地业务库 |

### `config/api_config.php`

| 常量 | 环境 | 含义 |
|---|---|---|
| `IS_TEST` | production: `0`, test: `1` | 环境标识 |
| `MAIN_SITE_PATH` | production: `G:/fileAnnexes`, test: `G:/fileAnnexesTest` | 本地物理文件路径前缀 |
| `FILE_SITE_PATH` | production: `https://file.usaeu.com`, test: `http://testfile.usaeu.com` | 文件访问 URL 前缀 |

---

## 与相关文件的协作关系

```
job/be_epr_pack_register.php          ← 本文档（阶段一：数据提取 + 写入本地库）
  │
  ├─ 写入 pack_be_register (status=0)
  │
  └─ [后续] 推送服务读取 pack_be_register (status=0)
       │  └─ 更新 status=2（推送完成）
       │
       └─ [后续] 更新 EPRRegInfo.PushTaxBureauStatus=3
```

参考实现文件:
- `vat_api_de_client/job/german_pack_register.php` — 德国包装法注册（数据提取、入库流程参考）
- `vat_api_de_client/job/extract_and_import_german_vat_data.php` — Base_AnnexesFile 取文件路径逻辑参考
