# EPR 数据处理器 - 技术设计文档

## 概述

EPR 数据处理器是一个持久化运行的 Laravel Command，用于自动处理意大利 EPR 业务记录。系统从 source 数据库读取待处理记录，执行数据转换、文档生成、文件上传等操作，并将结果保存到 target 数据库。

### 核心功能
- 持久化运行，每 8 秒轮询数据库
- 数据读取与验证（SQL Server source 数据库）
- 数据转换（电话格式化、姓名拆分、日期处理）
- DeepL API 翻译（带缓存和重试机制）
- Word 文档生成（基于模板）
- 腾讯云 COS 文件上传
- 结果保存（SQL Server target 数据库）
- 完善的错误处理和日志记录
- 数据库连接重试机制

### 技术栈
- Laravel 11.51.0 + PHP 8.3.25
- SQL Server（source 和 target 数据库）
- DeepL API（翻译服务）
- 腾讯云 COS SDK（对象存储）
- WordTemplateProcessor（Word 模板处理）
- Windows 环境 + PowerShell

## 架构设计

### 分层架构

系统采用 MVC + Service/Repository 分层架构：

```
┌─────────────────────────────────────────────────────────────┐
│                     Console Command Layer                    │
│                  (EprProcessItalyCommand)                    │
└──────────────────────────┬──────────────────────────────────┘
                           │
┌──────────────────────────┴──────────────────────────────────┐
│                      Service Layer                           │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐     │
│  │ EprProcessor │  │ Translation  │  │ Document     │     │
│  │ Service      │  │ Service      │  │ Generator    │     │
│  └──────────────┘  └──────────────┘  └──────────────┘     │
│  ┌──────────────┐  ┌──────────────┐                        │
│  │ FileUpload   │  │ DataTransform│                        │
│  │ Service      │  │ Service      │                        │
│  └──────────────┘  └──────────────┘                        │
└──────────────────────────┬──────────────────────────────────┘
                           │
┌──────────────────────────┴──────────────────────────────────┐
│                    Repository Layer                          │
│  ┌──────────────┐  ┌──────────────┐                        │
│  │ Source DB    │  │ Target DB    │                        │
│  │ Repository   │  │ Repository   │                        │
│  └──────────────┘  └──────────────┘                        │
└──────────────────────────┬──────────────────────────────────┘
                           │
┌──────────────────────────┴──────────────────────────────────┐
│                    External Services                         │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐     │
│  │ SQL Server   │  │ DeepL API    │  │ Tencent COS  │     │
│  │ (Source/     │  │              │  │              │     │
│  │  Target)     │  │              │  │              │     │
│  └──────────────┘  └──────────────┘  └──────────────┘     │
└─────────────────────────────────────────────────────────────┘
```


### 组件职责

#### Console Command Layer
- **EprProcessItalyCommand**: 持久化运行的 Artisan 命令，负责轮询调度和异常处理

#### Service Layer
- **EprProcessorService**: 核心业务编排服务，协调整个处理流程
- **TranslationService**: DeepL API 翻译服务，包含缓存和重试机制
- **DocumentGeneratorService**: Word 文档生成服务，封装 WordTemplateProcessor
- **FileUploadService**: 腾讯云 COS 文件上传服务
- **DataTransformService**: 数据转换服务（电话格式化、姓名拆分、日期处理）

#### Repository Layer
- **SourceDbRepository**: Source 数据库访问层，负责读取待处理记录和更新状态
- **TargetDbRepository**: Target 数据库访问层，负责保存处理结果

### 数据流程

```mermaid
graph TD
    A[启动 Command] --> B[每 8 秒轮询]
    B --> C{查询待处理记录}
    C -->|无记录| B
    C -->|有记录| D[读取记录数据]
    D --> E[数据转换]
    E --> F[电话格式化]
    E --> G[姓名拆分]
    E --> H[日期处理]
    F --> I[DeepL 翻译国家名]
    G --> I
    H --> I
    I --> J{翻译成功?}
    J -->|失败| K[重试 3 次]
    K -->|仍失败| L[停止程序]
    J -->|成功| M[生成 Word 文档]
    M --> N[上传到腾讯云 COS]
    N --> O{上传成功?}
    O -->|失败| P[更新 source 状态为 -1]
    O -->|成功| Q[保存到 target 数据库]
    Q --> R[更新 source 状态]
    R --> B
    P --> B

```

## 组件和接口

### 1. Console Command

#### EprProcessItalyCommand

```php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Services\Epr\EprProcessorService;
use Illuminate\Support\Facades\Log;

class EprProcessItalyCommand extends Command
{
    protected $signature = 'epr:process-italy';
    protected $description = '持久化运行的意大利 EPR 数据处理器';

    public function __construct(
        private EprProcessorService $processorService
    ) {
        parent::__construct();
    }

    public function handle(): int
    {
        Log::channel('epr')->info('EPR 处理器启动');
        
        while (true) {
            try {
                $this->processorService->processNextRecord();
                sleep(8); // 8 秒轮询间隔
            } catch (\Exception $e) {
                Log::channel('epr')->error('处理异常', [
                    'error' => $e->getMessage(),
                    'trace' => $e->getTraceAsString()
                ]);
                sleep(8);
            }
        }
        
        return Command::SUCCESS;
    }
}
```

### 2. Service Layer

#### EprProcessorService

核心业务编排服务，协调整个处理流程。

```php
<?php

namespace App\Services\Epr;

class EprProcessorService
{
    public function __construct(
        private SourceDbRepository $sourceRepo,
        private TargetDbRepository $targetRepo,
        private DataTransformService $transformer,
        private TranslationService $translator,
        private DocumentGeneratorService $docGenerator,
        private FileUploadService $uploader
    ) {}

    /**
     * 处理下一条待处理记录
     */
    public function processNextRecord(): void;

    /**
     * 处理单条记录的完整流程
     */
    private function processRecord(array $sourceData): void;

    /**
     * 处理失败时更新 source 状态
     */
    private function handleProcessingError(int $saasId, \Exception $e): void;
}
```

#### DataTransformService

数据转换服务，负责电话格式化、姓名拆分、日期处理。

```php
<?php

namespace App\Services\Epr;

class DataTransformService
{
    /**
     * 格式化电话号码为 00+区号+号码 格式
     * 
     * @param string|null $phone 原始电话号码
     * @return string|null 格式化后的电话号码
     */
    public function formatPhoneNumber(?string $phone): ?string;

    /**
     * 拆分法人姓名（拼音）为姓和名
     * 规则：最后一个空格后为姓（全大写），之前的为名（首字母大写）
     * 
     * @param string|null $fullName 完整姓名拼音
     * @return array{first_name: string|null, last_name: string|null}
     */
    public function splitLegalName(?string $fullName): array;

    /**
     * 拆分出生日期为年月日
     * 
     * @param string|null $birthDate 出生日期
     * @return array{day: int|null, month: int|null, year: int|null}
     */
    public function splitBirthDate(?string $birthDate): array;

    /**
     * 格式化日期为 DD/MM/YYYY 格式（用于 Word 模板）
     * 
     * @param string|null $date 日期字符串
     * @return string|null 格式化后的日期
     */
    public function formatDateForWord(?string $date): ?string;
}
```



#### TranslationService

DeepL API 翻译服务，包含缓存和重试机制。

```php
<?php

namespace App\Services\Epr;

class TranslationService
{
    private const CACHE_FILE = 'storage/app/translations/country_names_it.json';
    private const MAX_RETRIES = 3;
    private const RETRY_DELAYS = [2, 5, 10]; // 秒

    public function __construct(
        private string $apiKey,
        private string $apiUrl
    ) {}

    /**
     * 翻译国家名称为意大利语（带缓存）
     * 
     * @param string $countryNameEn 英文国家名
     * @return string 意大利语国家名
     * @throws TranslationException 3 次重试全部失败时抛出
     */
    public function translateCountryName(string $countryNameEn): string;

    /**
     * 从缓存加载翻译
     */
    private function loadCache(): array;

    /**
     * 保存翻译到缓存
     */
    private function saveCache(array $cache): void;

    /**
     * 调用 DeepL API（带重试机制）
     */
    private function callDeepLApi(string $text): string;
}
```

#### DocumentGeneratorService

Word 文档生成服务，封装 WordTemplateProcessor。

```php
<?php

namespace App\Services\Epr;

use Haiya\WordTemplateProcessor;

class DocumentGeneratorService
{
    private const TEMPLATE_PATH = '1.docx';
    private const TEMP_DIR = 'storage/app/temp';

    /**
     * 生成 EPR 说明书文档
     * 
     * @param array $data 文档数据
     * @return string 生成的文件路径
     * @throws DocumentGenerationException
     */
    public function generateEprDocument(array $data): string;

    /**
     * 准备模板占位符数据
     */
    private function prepareTemplateData(array $data): array;

    /**
     * 生成唯一的临时文件路径
     */
    private function generateTempFilePath(string $code): string;
}
```

#### FileUploadService

腾讯云 COS 文件上传服务。

```php
<?php

namespace App\Services\Epr;

use Qcloud\Cos\Client;

class FileUploadService
{
    private Client $cosClient;

    public function __construct(
        private string $secretId,
        private string $secretKey,
        private string $region,
        private string $bucket
    ) {
        $this->initializeCosClient();
    }

    /**
     * 上传文件到腾讯云 COS
     * 
     * @param string $localFilePath 本地文件路径
     * @param string $code 流水号
     * @return string 上传后的完整 URL
     * @throws FileUploadException
     */
    public function uploadToTencentCos(string $localFilePath, string $code): string;

    /**
     * 生成 COS 对象键（路径）
     * 格式: epr/italy/desc/{year}/{month}/{filename}
     */
    private function generateCosKey(string $code): string;

    /**
     * 初始化 COS 客户端
     */
    private function initializeCosClient(): void;
}
```



### 3. Repository Layer

#### SourceDbRepository

Source 数据库访问层。

```php
<?php

namespace App\Repositories\Epr;

use Illuminate\Support\Facades\DB;

class SourceDbRepository
{
    private const CONNECTION = 'source';
    private const MAX_RETRIES = 10;
    private const RETRY_INTERVAL = 5; // 秒

    /**
     * 获取下一条待处理记录
     * 
     * @return array|null
     */
    public function getNextPendingRecord(): ?array;

    /**
     * 更新记录状态为失败
     * 
     * @param int $saasId
     * @param string $errorMsg 错误信息（最多 100 字符）
     */
    public function updateRecordStatusToFailed(int $saasId, string $errorMsg): void;

    /**
     * 更新记录状态为成功
     * 
     * @param int $saasId
     */
    public function updateRecordStatusToSuccess(int $saasId): void;

    /**
     * 执行带重试的数据库查询
     */
    private function executeWithRetry(callable $callback): mixed;
}
```

#### TargetDbRepository

Target 数据库访问层。

```php
<?php

namespace App\Repositories\Epr;

use Illuminate\Support\Facades\DB;

class TargetDbRepository
{
    private const CONNECTION = 'target';
    private const MAX_RETRIES = 10;
    private const RETRY_INTERVAL = 5; // 秒

    /**
     * 保存处理结果到 target 数据库
     * 
     * @param array $data 处理后的数据
     * @return int 插入的记录 ID
     */
    public function saveProcessedRecord(array $data): int;

    /**
     * 执行带重试的数据库操作
     */
    private function executeWithRetry(callable $callback): mixed;
}
```

## 数据模型

### Source 数据库查询结果

从 source 数据库读取的原始数据结构：

```php
[
    'Id' => 12345,                              // saas_id
    'NameEng' => 'ABC Company Ltd',             // company_name_en
    'RegAddressEng' => '123 Main Street',       // company_address_en
    'CompanyAddressPostcode' => '100000',       // company_zip_code
    'Country' => 'China',                       // 需翻译
    'RegNumber' => 'REG123456',                 // vat_number
    'VATNumber' => 'IT12345678901',             // italy_vat_number
    'LegalPersonFullNamePinYin' => 'Shaohua SHI', // 需拆分
    'LegalPersonPhone' => '86-13488979214',     // 需格式化
    'LegalPersonEmail' => 'legal@example.com',  // company_email
    'LegalSignedFile' => 'file_id_123',         // legal_rep_signature_file_id
    'LegalPersonBirthDate' => '1982-02-28',     // 需拆分
    'CompanyAddressProvinceEn' => 'Beijing',    // company_province_en
    'CityEngName' => 'Beijing',                 // company_city_en
    'LegalPersonCityEngName' => 'Shanghai',     // legal_rep_birthplace
    'BusinessSerialNumber' => 'EPR20240115001', // code
    'F_Mobile' => '13800138000',                // sales_phone
]
```



### Target 数据库插入数据

保存到 target 数据库的处理后数据结构：

```php
[
    'saas_id' => 12345,
    'code' => 'EPR20240115001',
    
    // 公司信息
    'company_name_en' => 'ABC Company Ltd',
    'company_address_en' => '123 Main Street',
    'company_zip_code' => '100000',
    'company_city_en' => 'Beijing',
    'company_province_en' => 'Beijing',
    'company_country_it' => 'Cina',              // 翻译后
    'vat_number' => 'REG123456',
    'italy_vat_number' => 'IT12345678901',
    'company_tel' => '008613488979214',          // 格式化后
    'company_email' => 'legal@example.com',
    
    // 法人信息
    'legal_rep_last_name' => 'SHI',              // 拆分后
    'legal_rep_first_name' => 'Shaohua',         // 拆分后
    'legal_rep_birthplace' => 'Shanghai',
    'legal_rep_birth_day' => 28,                 // 拆分后
    'legal_rep_birth_month' => 2,                // 拆分后
    'legal_rep_birth_year' => 1982,              // 拆分后
    'legal_rep_signature_file_id' => 'file_id_123',
    
    // 文件日期（当天）
    'doc_day' => 15,
    'doc_month' => 1,
    'doc_year' => 2024,
    
    // 处理状态
    'desc_status' => 2,                          // 2=处理成功
    'desc_file_url' => 'https://cos.example.com/...', // COS URL
    'desc_process_time' => '2024-01-15 10:30:25',
    
    // 销售电话
    'sales_phone' => '008613800138000',          // 格式化后
]
```

### Word 模板数据

传递给 WordTemplateProcessor 的占位符数据：

```php
[
    '{{company_name}}' => 'ABC Company Ltd',
    '{{company_address}}' => '123 Main Street',
    '{{company_zip_code}}' => '100000',
    '{{company_city}}' => 'Beijing',
    '{{company_province}}' => 'Beijing',
    '{{company_country}}' => 'Cina',
    '{{vat_number}}' => 'REG123456',
    '{{company_tel}}' => '008613488979214',
    '{{company_email}}' => 'legal@example.com',
    '{{legal_rep_name}}' => 'Shaohua SHI',
    '{{legal_rep_birthplace}}' => 'Shanghai',
    '{{legal_rep_birthdate}}' => '28/02/1982',
    '{{doc_date}}' => '15/01/2024',
]
```

## 数据库配置

### 连接配置

在 `config/database.php` 中添加 source 和 target 数据库连接：

```php
'connections' => [
    // 现有连接...
    
    'source' => [
        'driver' => 'sqlsrv',
        'host' => env('SOURCE_DB_HOST'),
        'port' => env('SOURCE_DB_PORT', '1433'),
        'database' => env('SOURCE_DB_DATABASE'),
        'username' => env('SOURCE_DB_USERNAME'),
        'password' => env('SOURCE_DB_PASSWORD'),
        'charset' => 'utf8',
        'prefix' => '',
        'prefix_indexes' => true,
    ],
    
    'target' => [
        'driver' => 'sqlsrv',
        'host' => env('TARGET_DB_HOST'),
        'port' => env('TARGET_DB_PORT', '1433'),
        'database' => env('TARGET_DB_DATABASE'),
        'username' => env('TARGET_DB_USERNAME'),
        'password' => env('TARGET_DB_PASSWORD'),
        'charset' => 'utf8',
        'prefix' => '',
        'prefix_indexes' => true,
    ],
],
```

### Source 数据库查询 SQL

```sql
SELECT TOP 1
    vb.Id,
    bc.NameEng,
    bc.RegAddressEng,
    bc.CompanyAddressPostcode,
    bc.Country,
    bc.RegNumber,
    gt.VATNumber,
    bc.LegalPersonFullNamePinYin,
    bc.LegalPersonPhone,
    bc.LegalPersonEmail,
    bc.LegalSignedFile,
    bc.LegalPersonBirthDate,
    bc.CompanyAddressProvinceEn,
    bc.CityEngName,
    bc.LegalPersonCityEngName,
    vb.BusinessSerialNumber,
    bu.F_Mobile
FROM EPRBusinessRecord vb
INNER JOIN BusinessCompany bc ON vb.CompanyId = bc.Id
LEFT JOIN GoodsTransportation gt ON vb.Id = gt.BusinessId
LEFT JOIN BusinessUser bu ON vb.SalesId = bu.Id
WHERE vb.Country = 'IT' 
  AND vb.PushTaxBureauStatus = 5
ORDER BY vb.Id ASC
```

