# EPR 数据处理器 - 需求文档

> **⚠️ 状态说明（2026-09-22 补记）**
>
> 本文档是 **source 流程**（常驻进程轮询源库）的原始需求规格，**不覆盖 API 受理模式**（`POST /api/epr/italy`）。
> API 侧契约见 `docs/api.md`；中央对接文档见 `app_withdrawn/docs/UNIFIED_API_DESIGN.md` §4.4.6。
>
> §7 配置清单中的 `EPR_DB_RETRY_MAX` / `EPR_DB_RETRY_INTERVAL` / `EPR_DEEPL_RETRY_MAX` **从未实现**
> （全代码零引用），已于 2026-09-22 从 `.env` / `.env.example` 移除，**勿据本文加回**。
>
> 实现现状一律以 `docs/architecture.md` 与 `docs/runbook.md` 为准。

## 1. 功能概述

从 source 数据库读取待处理的 EPR 业务记录，生成意大利语说明书文档，上传到腾讯云 OSS，并将处理结果保存到 target 数据库。

## 2. 用户故事

### US-1: 数据读取与验证
**作为** 系统管理员  
**我想要** 从 source 数据库读取待处理的意大利 EPR 记录  
**以便** 进行后续的文档生成和数据处理

**验收标准：**
- 系统能够连接到 source 数据库（SQL Server）
- 能够执行指定的 SQL 查询获取数据
- 只读取 Country='IT' 且 PushTaxBureauStatus=5 的记录
- 每次处理一条记录（TOP 1）
- 读取的数据包含所有必需字段

**数据字段映射：**
```
vb.Id → saas_id
bc.NameEng → company_name_en
bc.RegAddressEng → company_address_en
bc.CompanyAddressPostcode → company_zip_code
bc.Country → 需翻译为意大利语 → company_country_it
bc.RegNumber → vat_number（CN/HK 取营业执照号不变；其他国家先取 VAT税号，为空回退营业执照号；不加国家前缀）
gt.VATNumber → italy_vat_number（仅非 CN/HK 公司；税号前面带字母时去掉前两位，如 IT12345678901→12345678901；可为空）
bc.LegalPersonFullNamePinYin → 需拆分为姓和名，前名后姓，如果有多个空格，最后一个是姓，前面的字符去除空格连接起来
bc.LegalPersonPhone → 需格式化 → company_tel
bc.LegalPersonEmail → company_email
bc.LegalSignedFile → legal_rep_signature_file_id
bc.LegalPersonBirthDate → 需拆分为年月日
bc.CompanyAddressProvinceEn → company_province_en
bc.CityEngName → company_city_en
bc.LegalPersonCityEngName → legal_rep_birthplace
vb.BusinessSerialNumber → code
bu.F_Mobile → sales_phone
```

### US-2: 电话号码格式化
**作为** 数据处理系统  
**我想要** 将各种格式的电话号码统一格式化  
**以便** 符合国际电话号码标准（00+区号+号码）

**验收标准：**
- 能够识别带区号的电话（如 "86-13488979214"）
- 能够识别不带区号的电话（如 "13488979214"）
- 输出格式统一为 "00" + 区号 + 号码（如 "008613488979214"）
- 如果无法识别区号，默认使用中国区号 86
- 去除所有非数字字符（除了作为分隔符的 "-"）

**测试用例：**
| 输入 | 输出 |
|------|------|
| 86-13488979214 | 008613488979214 |
| 13488979214 | 008613488979214 |
| +86 134 8897 9214 | 008613488979214 |
| 01012345678 | 00861012345678 |

### US-3: 国家名称翻译
**作为** 数据处理系统  
**我想要** 将国家英文名称翻译为意大利语  
**以便** 在意大利语文档中正确显示

**验收标准：**
- 使用 DeepL API 进行翻译
- 支持配置 DeepL API 密钥（免费版）
- 翻译结果缓存到本地文件
- 相同国家名称不重复调用 API
- 缓存文件格式为 JSON：`{"China": "Cina", "USA": "Stati Uniti"}`
- 缓存文件路径：`storage/app/translations/country_names_it.json`
- API 调用失败时自动重试，最多重试 3 次
- 重试间隔：第 1 次 2 秒，第 2 次 5 秒，第 3 次 10 秒
- 3 次重试全部失败后，停止整个程序运行并记录错误
- 记录每次重试的详细日志

### US-4: 法人姓名拆分
**作为** 数据处理系统  
**我想要** 将法人全名拼音拆分为姓和名  
**以便** 分别存储到数据库字段

**验收标准：**
- 输入格式：拼音全名，前名后姓（如 "Shaohua SHI"）
- 拆分规则：最后一个空格后为姓，之前的所有字符去除空格连接起来作为名
- 如果没有空格，整个字符串作为名，姓为空
- 去除首尾空格
- 大小写处理：
  - 姓：转换为全大写（如 SHI）
  - 名：首字母大写，其余小写（如 Shaohua）
  - 如果名有多个单词，每个单词首字母大写（如 Xiao Ming → Xiaoming）

**测试用例：**
| 输入 | 名（First Name） | 姓（Last Name） |
|------|------------------|-----------------|
| Shaohua SHI | Shaohua | SHI |
| shaohua shi | Shaohua | SHI |
| SHAOHUA SHI | Shaohua | SHI |
| Xiao Ming WANG | Xiaoming | WANG |
| xiao ming wang | Xiaoming | WANG |
| Li | Li | (空) |
| ZHANG | Zhang | (空) |

### US-5: 出生日期拆分与格式化
**作为** 数据处理系统  
**我想要** 将出生日期拆分为年、月、日并格式化  
**以便** 分别存储到数据库字段和填充到 Word 模板

**验收标准：**
- 输入格式：日期字符串或 DateTime 对象
- 支持的格式：YYYY-MM-DD, YYYY/MM/DD, DateTime
- 输出：年（SMALLINT）、月（TINYINT）、日（TINYINT）
- 无效日期返回 NULL
- Word 模板格式化：DD/MM/YYYY（如 28/02/1982）
- 日和月不足两位时前面补 0

**测试用例：**
| 输入 | 日 | 月 | 年 | Word 格式 |
|------|----|----|-----|-----------|
| 1982-02-28 | 28 | 2 | 1982 | 28/02/1982 |
| 2000-01-05 | 5 | 1 | 2000 | 05/01/2000 |
| 1995-12-31 | 31 | 12 | 1995 | 31/12/1995 |

### US-6: Word 文档生成
**作为** 数据处理系统  
**我想要** 根据模板生成填充数据的 Word 文档  
**以便** 作为说明书提交

**验收标准：**
- 使用根目录的 `1.docx` 作为模板
- 使用 `WordTemplateProcessor` 类处理
- 替换所有 `{{字段名}}` 占位符
- 生成的文件保存到临时目录：`storage/app/temp/epr_desc_{code}_{timestamp}.docx`
- 文件名包含流水号和时间戳以避免冲突
- 生成失败时抛出异常

**占位符映射：**
```
{{company_name}} → company_name_en
{{company_address}} → company_address_en
{{company_zip_code}} → company_zip_code
{{company_city}} → company_city_en
{{company_province}} → company_province_en
{{company_country}} → company_country_it
{{vat_number}} → vat_number
{{company_tel}} → company_tel
{{company_email}} → company_email
{{legal_rep_name}} → legal_rep_first_name + " " + legal_rep_last_name
{{legal_rep_birthplace}} → legal_rep_birthplace
{{legal_rep_birthdate}} → DD/MM/YYYY 格式（如 28/02/1982）
{{doc_date}} → DD/MM/YYYY 格式（当天日期）
```

**日期格式化规则：**
- 格式：DD/MM/YYYY
- 日和月不足两位时前面补 0
- 示例：2024-01-05 → 05/01/2024

### US-7: 文件上传到腾讯云 OSS
**作为** 数据处理系统  
**我想要** 将生成的 Word 文档上传到腾讯云 OSS  
**以便** 永久存储并获取访问链接

**验收标准：**
- 支持配置腾讯云 OSS 参数（SecretId, SecretKey, Region, Bucket）
- 配置存储在 `.env` 文件中
- 上传路径：`epr/italy/desc/{year}/{month}/{filename}`
- 文件名格式：`{code}_{timestamp}.docx`
- 上传成功返回完整的 URL
- 上传失败抛出异常
- 上传成功后删除本地临时文件

**配置项：**
```
TENCENT_COS_SECRET_ID=
TENCENT_COS_SECRET_KEY=
TENCENT_COS_REGION=ap-guangzhou
TENCENT_COS_BUCKET=
```

### US-8: 数据保存到 target 数据库
**作为** 数据处理系统  
**我想要** 将处理后的数据保存到 target 数据库  
**以便** 记录处理结果

**验收标准：**
- 连接到 target 数据库（SQL Server）
- 插入数据到 `italia_epr_file` 表
- 设置 `desc_status = 2`（处理成功）
- 设置 `desc_file_url` 为 OSS 返回的 URL
- 设置 `desc_process_time` 为当前时间
- 设置 `doc_day`, `doc_month`, `doc_year` 为当天日期
- 使用事务确保数据一致性

### US-9: 错误处理与状态更新
**作为** 数据处理系统  
**我想要** 在处理失败时记录详细错误信息  
**以便** 追踪问题并通知相关人员

**验收标准：**
- 任何步骤失败时捕获异常
- 记录详细错误日志到 Laravel 日志系统
- 更新 source 数据库 `EPRBusinessRecord` 表：
  - `PushTaxBureauStatus = -1`
  - `PushTaxBureauErrorMsg = 错误摘要（最多 100 字符）`
- 错误信息超过 100 字符时自动截断
- 错误信息包含：错误类型、关键参数、时间戳
- 如果是 target 数据库插入失败，也要更新 source 状态

**错误信息格式：**
```
[{步骤}] {错误类型}: {简要描述} ({时间})
```

**示例：**
```
[文档生成] 模板文件不存在: 1.docx (2024-01-15 10:30:25)
[OSS上传] 认证失败: 无效的SecretId (2024-01-15 10:30:26)
```

### US-10: 日志记录
**作为** 系统管理员  
**我想要** 查看详细的处理日志  
**以便** 监控系统运行状态和排查问题

**验收标准：**
- 使用 Laravel 日志系统
- 日志级别：
  - INFO: 处理开始、处理成功、程序启动、数据库连接成功
  - WARNING: 数据缺失、数据库连接失败（重连前）
  - ERROR: 处理失败、异常、DeepL API 重试失败
- 日志内容包含：
  - 记录 ID（saas_id）
  - 流水号（code）
  - 处理步骤
  - 关键参数
  - 执行时间
- 日志文件：`storage/logs/epr-processor.log`

### US-11: 持久化运行与数据库轮询
**作为** 系统管理员  
**我想要** 程序持久化在内存中运行并定期检查待处理数据  
**以便** 自动处理新的 EPR 记录

**验收标准：**
- 程序以常驻进程方式运行（Laravel Command）
- 每隔 8 秒查询一次 source 数据库
- 查询条件：`Country='IT' AND PushTaxBureauStatus=5`
- 每次只处理一条记录（TOP 1）
- 如果没有待处理记录，继续等待下一次轮询
- 程序启动时记录启动日志
- 支持优雅停止（Ctrl+C）

### US-12: 数据库连接重试机制
**作为** 系统管理员  
**我想要** 在数据库连接失败时自动重连  
**以便** 保证程序稳定运行

**验收标准：**
- 数据库连接失败时自动重试
- 重试策略：
  - 每隔 5 秒重试一次
  - 最多重试 10 次
  - 总计尝试时间：50 秒
- 每次重试前记录警告日志
- 10 次重试全部失败后，记录错误日志并退出程序
- 程序退出后由 NSSM 服务自动重启
- 连接成功后记录恢复日志
- 适用于 source 和 target 数据库

### US-13: Windows 环境适配
**作为** 开发人员  
**我想要** 程序在 Windows 环境下正常运行  
**以便** 部署到 Windows 服务器

**验收标准：**
- 使用 Windows 兼容的路径分隔符（反斜杠 `\`）
- 文件换行符使用 CRLF
- 使用 PowerShell 命令启动程序
- 支持 Windows 服务方式运行（可选）
- 临时文件路径使用 Windows 兼容格式
- 日志文件路径使用 Windows 兼容格式

**启动命令（PowerShell）：**
```powershell
php artisan epr:process-italy
```

**Windows 服务配置（可选）：**
- 使用 NSSM（Non-Sucking Service Manager）或类似工具
- 服务名称：ItalyEPRProcessor
- 自动启动
- 失败后自动重启

## 3. 正确性属性

### P-1: 数据完整性
**属性：** 从 source 读取的每条记录必须完整保存到 target 或标记为失败  
**验证方法：** 
- 检查 source 中 `PushTaxBureauStatus=5` 的记录数
- 检查 target 中对应的记录数 + source 中 `PushTaxBureauStatus=-1` 的记录数
- 两者之和应等于原始待处理记录数

### P-2: 电话号码格式一致性
**属性：** 所有保存的电话号码必须符合 "00" + 数字 的格式  
**验证方法：**
- 正则表达式：`^00\d{10,15}$`
- 所有 `company_tel` 和 `sales_phone` 字段必须匹配此模式

### P-3: 文件上传幂等性
**属性：** 相同的文档不应重复上传到 OSS  
**验证方法：**
- 使用 `code` + `timestamp` 生成唯一文件名
- 上传前检查文件是否已存在（可选）

### P-4: 错误状态一致性
**属性：** 处理失败时，source 状态必须更新为 -1  
**验证方法：**
- 模拟各种失败场景
- 验证 `PushTaxBureauStatus` 是否正确更新
- 验证 `PushTaxBureauErrorMsg` 是否包含错误信息

### P-5: 翻译缓存有效性
**属性：** 相同的国家名称应使用缓存，不重复调用 API  
**验证方法：**
- 处理多条相同国家的记录
- 验证 DeepL API 只调用一次
- 验证缓存文件正确更新

### P-6: DeepL API 重试可靠性
**属性：** DeepL API 调用失败时必须重试 3 次，全部失败后停止程序  
**验证方法：**
- 模拟 API 调用失败
- 验证重试次数为 3 次
- 验证重试间隔符合预期（2s, 5s, 10s）
- 验证 3 次失败后程序停止

### P-7: 数据库连接恢复能力
**属性：** 数据库连接失败时必须重试 10 次，全部失败后退出程序  
**验证方法：**
- 模拟数据库连接中断
- 验证自动重连机制启动
- 验证重试次数为 10 次
- 验证重试间隔为 5 秒
- 验证 10 次失败后程序退出

### P-8: 持久化运行稳定性
**属性：** 程序必须持续运行，每 8 秒轮询一次数据库  
**验证方法：**
- 启动程序并运行 1 小时
- 验证轮询间隔为 8 秒
- 验证内存使用稳定（无内存泄漏）
- 验证日志正常记录

## 4. 边界条件

### BC-1: 空数据处理
- 法人姓名为空：姓和名都设为 NULL
- 电话号码为空：保存为 NULL
- 出生日期为空：年月日都设为 NULL
- 国家名称为空：使用原值，不翻译

### BC-2: 特殊字符处理
- 公司地址超过 100 字符：截断到 100 字符
- 错误信息超过 100 字符：截断到 100 字符
- Word 模板中的特殊字符：使用 `WordTemplateProcessor` 的 XML 转义

### BC-3: 并发处理
- 使用数据库锁防止重复处理同一条记录
- 临时文件使用唯一文件名避免冲突
- OSS 上传使用唯一路径避免覆盖

### BC-4: 网络异常
- DeepL API 超时：重试 3 次（间隔 2s, 5s, 10s），全部失败后停止程序
- OSS 上传超时：重试 3 次，失败后标记错误
- 数据库连接失败：每隔 5 秒重试一次，最多 10 次，全部失败后退出程序

### BC-5: 长时间运行
- 程序持续运行不重启
- 内存使用保持稳定（< 500MB）
- 数据库连接池自动管理
- 日志文件自动轮转（每天一个文件）

## 5. 非功能需求

### NFR-1: 性能
- 单条记录处理时间 < 10 秒（不含网络延迟）
- DeepL API 调用超时：5 秒
- OSS 上传超时：30 秒
- 数据库查询超时：10 秒
- 轮询间隔：8 秒

### NFR-2: 可靠性
- 使用数据库事务确保数据一致性
- 所有外部调用都有异常处理
- 失败时正确回滚状态
- DeepL API 失败重试 3 次
- 数据库连接失败重试 10 次（每次间隔 5 秒）
- 程序退出后由 NSSM 服务自动重启

### NFR-3: 可维护性
- 遵循 PSR-12 编码标准
- 使用 Service/Repository 分层架构
- 所有公共方法都有 PHPDoc 注释
- 配置项集中在 `.env` 文件
- 详细的日志记录

### NFR-4: 安全性
- API 密钥存储在 `.env` 文件，不提交到版本控制
- 数据库连接使用参数化查询
- 上传文件路径使用白名单验证

### NFR-5: 可用性
- 程序持久化运行，无需手动触发
- 支持优雅停止和重启
- 数据库连接自动恢复
- 适配 Windows 环境

## 6. 依赖与约束

### 依赖
- Laravel 11.51.0
- PHP 8.3.25
- SQL Server 数据库
- 腾讯云 COS SDK
- DeepL API（免费版）
- WordTemplateProcessor 类（已存在）

### 约束
- Windows 环境 + PowerShell
- 使用 Eloquent ORM
- 遵循 MVC + Service/Repository 架构
- 使用 PSR-12 编码标准
- 程序以常驻进程方式运行
- 每 8 秒轮询一次数据库

## 7. 配置项

### 数据库配置（config/database.php）
```php
'source' => [
    'driver' => 'sqlsrv',
    'host' => env('SOURCE_DB_HOST'),
    'database' => env('SOURCE_DB_DATABASE'),
    'username' => env('SOURCE_DB_USERNAME'),
    'password' => env('SOURCE_DB_PASSWORD'),
],
'target' => [
    'driver' => 'sqlsrv',
    'host' => env('TARGET_DB_HOST'),
    'database' => env('TARGET_DB_DATABASE'),
    'username' => env('TARGET_DB_USERNAME'),
    'password' => env('TARGET_DB_PASSWORD'),
],
```

### 环境变量（.env）
```
# Source Database
SOURCE_DB_HOST=
SOURCE_DB_DATABASE=
SOURCE_DB_USERNAME=
SOURCE_DB_PASSWORD=

# Target Database
TARGET_DB_HOST=
TARGET_DB_DATABASE=
TARGET_DB_USERNAME=
TARGET_DB_PASSWORD=

# DeepL API
DEEPL_API_KEY=
DEEPL_API_URL=https://api-free.deepl.com/v2/translate

# Tencent COS
TENCENT_COS_SECRET_ID=
TENCENT_COS_SECRET_KEY=
TENCENT_COS_REGION=ap-guangzhou
TENCENT_COS_BUCKET=

# EPR Processor
EPR_WORD_TEMPLATE_PATH=1.docx
EPR_TEMP_DIR=storage/app/temp
EPR_TRANSLATION_CACHE_PATH=storage/app/translations/country_names_it.json
EPR_POLL_INTERVAL=8
EPR_DB_RETRY_MAX=10
EPR_DB_RETRY_INTERVAL=5
EPR_DEEPL_RETRY_MAX=3
```

## 8. 运行方式

### 开发环境
```powershell
# 启动处理器（前台运行）
php artisan epr:process-italy

# 查看日志
Get-Content storage\logs\epr-processor.log -Tail 50 -Wait
```

### 生产环境（Windows 服务）
```powershell
# 使用 NSSM 安装服务
nssm install ItalyEPRProcessor "C:\path\to\php.exe" "artisan epr:process-italy"
nssm set ItalyEPRProcessor AppDirectory "C:\path\to\project"
nssm set ItalyEPRProcessor AppStdout "C:\path\to\project\storage\logs\service-output.log"
nssm set ItalyEPRProcessor AppStderr "C:\path\to\project\storage\logs\service-error.log"

# 配置自动重启（程序退出后自动重启）
nssm set ItalyEPRProcessor AppExit Default Restart
nssm set ItalyEPRProcessor AppRestartDelay 5000

# 启动服务
nssm start ItalyEPRProcessor

# 停止服务
nssm stop ItalyEPRProcessor

# 查看服务状态
nssm status ItalyEPRProcessor
```

**说明：**
- 数据库连接失败 10 次后程序会自动退出
- NSSM 服务会在程序退出后 5 秒自动重启
- 这样可以避免长时间等待，快速恢复服务
