# 比利时包装法注册 自动化开发文档

## 1. 概述

| 项目 | 内容 |
|------|------|
| **项目名称** | 比利时包装法注册自动化 |
| **所属项目** | `vat_api_se_client` |
| **数据来源** | SQL Server（SaaS 系统） |
| **数据目标** | SQL Server 本地库 `pack_be_register` 表 |
| **文件生成** | 调用 `server/be_epr_pack_auto_file.php` 接口（已实现） |
| **参考实现** | `vat_api_de_client/job/german_pack_register.php`（数据提取与入库）<br>`vat_api_de_client/job/extract_and_import_german_vat_data.php`（文件路径获取） |

---

## 2. 业务流程

```
┌─────────────────────────────────────────────────────────────┐
│                    阶段一：生成文件                           │
├─────────────────────────────────────────────────────────────┤
│  1. 查询 SaaS 库中符合条件的比利时包装法注册数据              │
│     - Country = 'BE'                                        │
│     - RecyclingType = '包装法'                               │
│     - ServiceItem = '包装法注册'                             │
│     - PushTaxBureauStatus = 1（待生成文件）                   │
│     - 业务状态 not in (已完成/已取消/待提交资料)               │
│                                                              │
│  2. 逐条调用 be_epr_pack_auto_file.php 接口                  │
│     - 传参：id = EPRRegInfo.ID                               │
│     - 返回：Mandat URL + membership URL                      │
│                                                              │
│  3. 从 Base_AnnexesFile 表获取已生成的文件路径                │
│     - InfoId = EPRRegInfo.ID                                 │
│     - FileCategoryId 对应 Mandat 和 membership               │
│                                                              │
│  4. 将数据写入 pack_be_register 表                      │
│     - Power_of_attorney  ← Mandat 文件路径                   │
│     - Membership_agreement ← membership 文件路径             │
│     - 其他字段从公司信息表映射                                │
│                                                              │
│  5. 更新 EPRRegInfo.PushTaxBureauStatus = 2（待推送）         │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│                    阶段二：推送官方                           │
├─────────────────────────────────────────────────────────────┤
│  1. 查询本地库 pack_be_register 表                      │
│     - status = 0（待推送）                                    │
│                                                              │
│  2. 推送文件到比利时官方系统                                  │
│     - 提交 Power_of_attorney（授权书）                        │
│     - 提交 Membership_agreement（会员合同）                   │
│                                                              │
│  3. 更新状态                                                  │
│     - 成功 → status = 2（推送完成）                           │
│     - 失败 → status = -1 + 记录 msg                          │
│                                                              │
│  4. 回写 SaaS 库 EPRRegInfo.PushTaxBureauStatus               │
│     - 成功 → PushTaxBureauStatus = 3（已完成）                │
│     - 失败 → 不更新，等待重试                                 │
└─────────────────────────────────────────────────────────────┘
```

---

## 3. 文件结构

```
vat_api_se_client/
├── job/
│   └── be_epr_pack_register.php    # 比利时包装法注册定时任务（待创建）
├── server/
│   └── be_epr_pack_auto_file.php    # 文件生成接口（已实现）
├── config/
│   ├── api_config.php               # API 配置
│   └── database_config.php          # 数据库配置
├── db/
│   └── sql_server_db.php            # SQL Server 数据库操作类
├── public/
│   ├── function.php                 # 公共函数
│   └── ossfile.php                  # OSS 文件上传
└── doc/
    ├── 自动化表.txt                  # pack_be_register 表结构
    └── belgium_pack_register_开发文档.md  # 本文档
```

---

## 4. 数据库设计

### 4.1 目标表：`pack_be_register`

建表语句见 `doc/自动化表.txt`。与比利时包装法注册相关的关键字段：

| 字段名 | 类型 | 说明 | 数据来源 |
|--------|------|------|----------|
| `tid` | NVARCHAR(50) | 订单流水号 | `EPRBusinessRecord.BusinessSerialNumber` |
| `VATBusinessID` | NVARCHAR(50) | 订单流水号 ID | `EPRBusinessRecord.ID` |
| `EPRRegInfoID` | NVARCHAR(50) | EPR 注册信息 ID | `EPRRegInfo.ID` |
| `Company_Name` | NVARCHAR(60) | 公司英文名称 | `Base_Customer_Company.NameEng` |
| `StreetNrBox` | NVARCHAR(150) | 公司地址英文 | `Base_Customer_Company.CompanyAddressLine1En` |
| `Zipcode` | NVARCHAR(20) | 邮编 | `Base_Customer_Company.CompanyAddressPostcode` |
| `City` | NVARCHAR(100) | 城市 | `Base_Customer_Company.CityEngName` |
| `Country` | NVARCHAR(100) | 国家 | `Base_Customer_Company.Country` |
| `Phone_number` | NVARCHAR(50) | 电话号码 | `Base_Customer_Company.LegalPersonPhone` |
| `VAT_or_company_number` | NVARCHAR(50) | 营业执照号 | `Base_Customer_Company.RegNumber` |
| `VAT_nr` | NVARCHAR(50) | 欧盟税号 | `Base_Customer_Company.RegNumber` |
| `Legal_LastName` | NVARCHAR(100) | 法人姓 | `LegalPersonFullNamePinYin` 解析 |
| `Legal_FirstName` | NVARCHAR(100) | 法人名 | `LegalPersonFullNamePinYin` 解析 |
| `Legal_Salutation` | NVARCHAR(10) | 性别（Mrs/Mr） | `LegalPersonGender` 转换 |
| `Legal_Email` | NVARCHAR(255) | 法人邮箱 | `Base_Customer_Company.LegalPersonEmail` |
| `Contact_LastName` | NVARCHAR(100) | 联系人姓 | 同法人 |
| `Contact_FirstName` | NVARCHAR(100) | 联系人名 | 同法人 |
| `Contact_Language` | NVARCHAR(50) | 语言 | 固定值 `English` |
| `Contact_Salutation` | NVARCHAR(10) | 联系人性别 | 同法人 |
| `Contact_Email` | NVARCHAR(255) | 联系人邮箱 | 同法人 |
| `Power_of_attorney` | NVARCHAR(500) | **授权书（Mandat）文件路径** | `Base_AnnexesFile` 或接口返回 |
| `Membership_agreement` | NVARCHAR(500) | **会员合同（membership）文件路径** | `Base_AnnexesFile` 或接口返回 |
| `msg` | NVARCHAR(2000) | 处理消息 | 错误时记录 |
| `status` | INT | 状态：0=待推送，2=推送完成，-1=失败 | |
| `retry_count` | INT | 重试次数 | |

### 4.2 源数据查询（SaaS 库）

```sql
SELECT 
    br.BusinessSerialNumber,
    br.ID AS RegisterID,
    ei.ID AS EPRRegInfoID,
    cc.*,
    c.CountryName_en,
    c.CountryTwoCode
FROM EPRBusinessRecord br
LEFT JOIN EPRRegInfo ei ON br.ID = ei.EPRBusinessRecordId
LEFT JOIN Base_Customer_Company cc ON cc.ID = br.CompanyId
LEFT JOIN RecyclingType rt ON rt.ID = br.RecyclingTypeId
LEFT JOIN Country c ON c.CountryName = cc.Country
LEFT JOIN ServiceItems si ON br.ServiceItemId = si.ID
WHERE br.Country = 'BE'
  AND ei.PushTaxBureauStatus = 1              -- 待生成文件
  AND ei.PushType = '301'                      -- 包装法注册类型
  AND rt.TypeName = '包装法'
  AND si.ServiceItemName = '包装法注册'
```

### 4.3 查询已生成文件路径（Base_AnnexesFile）

参考 `extract_and_import_german_vat_data.php` 的方式，从 `Base_AnnexesFile` 表获取文件路径：

```sql
-- 查询授权书（Mandat）
SELECT F_FilePath 
FROM Base_AnnexesFile 
WHERE InfoId = :EPRRegInfoID 
  AND FileCategoryId = :mandatCategoryId

-- 查询会员合同（membership）  
SELECT F_FilePath 
FROM Base_AnnexesFile 
WHERE InfoId = :EPRRegInfoID 
  AND FileCategoryId = :membershipCategoryId
```

需确认 Mandat 和 membership 各自对应的 `FileCategoryId` 值（GUID）。

---

## 5. 文件生成接口调用

### 5.1 接口说明

| 项目 | 内容 |
|------|------|
| **接口地址** | `{base_url}/server/be_epr_pack_auto_file.php` |
| **请求方式** | POST |
| **Content-Type** | application/json |

### 5.2 请求参数

```json
{
    "id": 12345
}
```

| 参数 | 类型 | 说明 |
|------|------|------|
| `id` | int | EPRRegInfo 表主键 ID |

### 5.3 成功响应

```json
{
    "code": 200,
    "msg": "success",
    "data": [
        {
            "Mandat": "https://oss.example.com/be_epr/POA_of_CompanyName.pdf",
            "membership": "https://oss.example.com/be_epr/FostPlus_Membership.pdf"
        }
    ]
}
```

### 5.4 调用示例（PHP）

```php
/**
 * 调用文件生成接口
 * @param int $eprRegInfoId EPRRegInfo 表主键 ID
 * @return array ['success' => bool, 'mandat' => string, 'membership' => string, 'message' => string]
 */
private function callGenerateFileApi($eprRegInfoId) {
    $apiUrl = BE_EPR_AUTO_FILE_API_URL; // 在 config/api_config.php 中配置
    
    $postData = json_encode(['id' => $eprRegInfoId]);
    
    $ch = curl_init($apiUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/json',
        'Content-Length: ' . strlen($postData)
    ]);
    curl_setopt($ch, CURLOPT_TIMEOUT, 60); // 文件生成长时间操作
    
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curlError = curl_error($ch);
    curl_close($ch);
    
    if ($curlError) {
        return ['success' => false, 'message' => "CURL错误: {$curlError}"];
    }
    
    $result = json_decode($response, true);
    
    if ($result && isset($result['code']) && $result['code'] === 200 && !empty($result['data'])) {
        return [
            'success' => true,
            'mandat' => $result['data'][0]['Mandat'] ?? '',
            'membership' => $result['data'][0]['membership'] ?? '',
            'message' => ''
        ];
    }
    
    return [
        'success' => false,
        'message' => $result['msg'] ?? '接口返回异常'
    ];
}
```

---

## 6. 定时任务实现

### 6.1 类结构

文件：`job/be_epr_pack_register.php`

```php
<?php
/**
 * 比利时包装法注册自动化任务
 * 
 * 阶段一：查询待生成文件的比利时包装法数据，调用 be_epr_pack_auto_file.php 生成文件，
 *         获取文件路径写入 pack_be_register 表，更新 PushTaxBureauStatus = 2
 * 阶段二：查询待推送的记录，推送官方，更新状态
 */

require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../db/sql_server_db.php';
require_once __DIR__ . '/../config/database_config.php';
require_once __DIR__ . '/../config/api_config.php';
require_once __DIR__ . '/../public/function.php';

class BelgiumPackRegister {
    /** @var SqlServerDb SaaS 系统数据库 */
    private $sqlServerDb;
    /** @var SqlServerDb 本地业务数据库 */
    private $localSqlServerDb;
    
    /**
     * 运行模式：'generate'=文件生成, 'push'=推送官方, 'all'=全部
     */
    private $mode;
    
    public function __construct($mode = 'all') {
        $this->mode = $mode;
    }
    
    /**
     * 执行主程序
     */
    public function run() {
        logMessage("===== 比利时包装法注册自动化任务开始 =====");
        
        try {
            $this->initializeDatabaseConnections();
            
            if ($this->mode === 'generate' || $this->mode === 'all') {
                $this->processFileGeneration();
            }
            
            if ($this->mode === 'push' || $this->mode === 'all') {
                $this->processPushToOfficial();
            }
        } catch (Exception $e) {
            logMessage("程序执行出错: " . $e->getMessage(), [], "error");
        } finally {
            $this->closeDatabaseConnections();
        }
        
        logMessage("===== 比利时包装法注册自动化任务结束 =====");
    }
    
    /**
     * 阶段一：文件生成
     */
    private function processFileGeneration() {
        // 查询待生成文件的数据
        $records = $this->queryPendingFileGeneration();
        
        if (empty($records)) {
            logMessage("没有需要生成文件的比利时包装法注册数据");
            return;
        }
        
        logMessage("找到 " . count($records) . " 条数据需要生成文件");
        
        foreach ($records as $record) {
            try {
                // 1. 调用接口生成文件
                $apiResult = $this->callGenerateFileApi($record['EPRRegInfoID']);
                
                if (!$apiResult['success']) {
                    $this->updateEPRRegInfoStatus($record['EPRRegInfoID'], -1, $apiResult['message']);
                    continue;
                }
                
                // 2. 从 Base_AnnexesFile 获取文件路径（备用方案）
                $filePaths = $this->getFilePathsFromAnnexes($record['EPRRegInfoID']);
                
                // 优先使用接口返回的 OSS URL，其次使用 Base_AnnexesFile 路径
                $mandatPath = $apiResult['mandat'] ?: ($filePaths['mandat'] ?? '');
                $membershipPath = $apiResult['membership'] ?: ($filePaths['membership'] ?? '');
                
                // 3. 写入 pack_be_register 表
                $this->insertOrUpdateRegistrationInfo($record, $mandatPath, $membershipPath);
                
                // 4. 更新 PushTaxBureauStatus = 2（待推送）
                $this->updateEPRRegInfoStatus($record['EPRRegInfoID'], 2, '');
                
                logMessage("文件生成成功: BE-" . $record['BusinessSerialNumber']);
                
            } catch (Exception $e) {
                logMessage("记录处理失败: " . $e->getMessage(), [], "error");
                $this->updateEPRRegInfoStatus($record['EPRRegInfoID'], -1, $e->getMessage());
            }
        }
    }
    
    /**
     * 阶段二：推送官方
     */
    private function processPushToOfficial() {
        // 查询待推送的记录
        $records = $this->queryPendingPush();
        
        if (empty($records)) {
            logMessage("没有需要推送官方的比利时包装法注册数据");
            return;
        }
        
        logMessage("找到 " . count($records) . " 条数据需要推送官方");
        
        foreach ($records as $record) {
            try {
                // TODO: 实际推送逻辑（对接比利时官方系统）
                // $pushResult = $this->pushToBelgiumOfficial($record);
                
                // 推送成功后更新状态
                // $this->updateLocalStatus($record['ID'], 2, '推送成功');
                // $this->updateEPRRegInfoStatus($record['EPRRegInfoID'], 3, '');
                
            } catch (Exception $e) {
                logMessage("推送失败: " . $e->getMessage(), [], "error");
                $this->updateLocalStatus($record['ID'], -1, $e->getMessage());
                $this->updateEPRRegInfoStatus($record['EPRRegInfoID'], -1, $e->getMessage());
            }
        }
    }
    
    // ... 后续实现方法
}
```

### 6.2 定时任务入口

```php
// 文件末尾，以守护进程模式运行
if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) {
    logMessage("比利时包装法注册服务启动，每 5 分钟执行一次");
    
    while (true) {
        $register = new BelgiumPackRegister('all');
        $register->run();
        
        logMessage("等待 5 分钟后再次执行");
        sleep(300);
    }
}
```

---

## 7. 状态流转

### 7.1 SaaS 端 EPRRegInfo.PushTaxBureauStatus

```
    1                2                 3
待生成文件 ────→ 待推送官方 ────→ 推送完成
    │                │
    └──── -1 ────────┘
       生成失败/推送失败
```

| 值 | 含义 | 触发条件 |
|----|------|----------|
| 1 | 待生成文件 | 订单创建后初始状态 |
| 2 | 待推送官方 | 文件生成成功后 |
| 3 | 推送完成 | 官方推送成功后 |
| -1 | 失败 | 文件生成失败或推送失败 |

### 7.2 本地 pack_be_register.status

| 值 | 含义 | 触发条件 |
|----|------|----------|
| 0 | 待处理 | 写入初始状态 |
| 1 | 处理中 | 正在推送官方（可选） |
| 2 | 推送完成 | 官方推送成功 |
| -1 | 失败 | 推送或处理失败 |

---

## 8. 错误处理

| 场景 | 处理策略 |
|------|----------|
| SaaS 库连接失败 | 抛出异常，终止本次执行，等待下次定时触发 |
| 未查询到待处理数据 | 正常跳过，等待下次 |
| 文件生成接口调用失败 | 更新 EPRRegInfo.status = -1，记录错误信息到 msg |
| Base_AnnexesFile 未找到文件 | 如接口已返回 URL 则使用 URL；否则标记失败 |
| pack_be_register 写入失败 | 记录日志，更新 EPRRegInfo.status = -1 |
| 推送官方失败 | 更新本地 status = -1，记录 msg，不更新 SaaS 端状态（允许重试） |
| 网络超时 | CURL 设置合理超时时间（60s），失败记录日志等待重试 |

---

## 9. 开发步骤

| 序号 | 任务 | 说明 |
|------|------|------|
| 1 | 确认 Base_AnnexesFile 中 Mandat 和 membership 的 FileCategoryId | 查询数据库确认 GUID 值 |
| 2 | 在 `config/api_config.php` 配置接口 URL | 添加 `BE_EPR_AUTO_FILE_API_URL` |
| 3 | 在 `db/sql_server_db.php` 添加状态更新方法 | `updateEPRRegInfoStatus()` / `updateLocalStatus()` |
| 4 | 创建 `job/belgium_pack_register.php` | 实现数据查询、接口调用、文件路径获取、入库 |
| 5 | 测试阶段一（文件生成） | 调用接口 → 获取文件路径 → 写入 pack_be_register |
| 6 | 测试阶段二（推送官方） | 读取本地库 → 推送 → 更新状态 |
| 7 | 配置 Windows 定时任务或守护进程 | 按需设置执行频率 |
| 8 | 监控日志 | 检查 `logs/` 目录下的日志文件 |

---

## 10. 配置项

在 `config/api_config.php` 中添加：

```php
// 比利时 EPR 自动生成文件接口
define('BE_EPR_AUTO_FILE_API_URL', 'http://localhost/vat_api_se_client/server/be_epr_pack_auto_file.php');
```

---

## 11. 日志

日志文件存放于 `vat_api_se_client/logs/` 目录，通过 `function.php` 中的 `logMessage()` 函数记录。

日志格式：
```
[2026-06-12 10:30:15] 比利时包装法注册服务启动，每 5 分钟执行一次
[2026-06-12 10:30:15] 找到 3 条数据需要生成文件
[2026-06-12 10:30:30] 文件生成成功: BE-POVAT20251202000021
[2026-06-12 10:30:32] 文件生成成功: BE-POVAT20251202000022
```

---

**文档版本**: v1.0  
**最后更新**: 2026-06-12  
**维护者**: 开发团队
