# 奥地利 EPR（包装/电池/WEEE）授权书自动生成 开发文档

## 1. 概述

| 项目 | 内容 |
|------|------|
| **项目名称** | 奥地利 EPR 授权书（POA）自动生成 |
| **所属项目** | `vat_api_se_client` |
| **数据来源** | SQL Server（SaaS 系统） |
| **数据目标** | SQL Server 本地库（拟新建 `pack_at_register`，待确认，见第 9 节） |
| **文件生成** | 改造 `job/at_epr_pack_auto_file.php`（在现有任务内直接生成） |
| **模板目录** | `resources/template/`（AT_PACK_POA.docx / AT_Battery_POA.docx / AT_WEEE_POA.docx） |
| **OSS 路径** | `AT_OSS_EPR_FILE_PATH`（`config/api_config.php` 已定义：生产 `vat_factory/at_epr_file/`，测试 `vat_fix/at_epr_file/`） |
| **参考实现** | `server/be_epr_pack_auto_file.php`（Word 模板替换、转 PDF、上传 OSS 流程） |

---

## 2. 业务流程

```
┌──────────────────────────────────────────────────────────────┐
│                    阶段一：自动生成授权书                       │
├──────────────────────────────────────────────────────────────┤
│  1. 查询 SaaS 库中符合条件的奥地利 EPR 注册数据                │
│     - br.Country = 'AT'                                      │
│     - ei.PushTaxBureauStatus = 1（待生成文件）                │
│     - ei.PushType = '301'                                    │
│     - rt.TypeName IN (包装法, 电池法, WEEE)  ← 三种都要查     │
│                                                              │
│  2. 逐条根据 rt.TypeName 选择模板：                           │
│     - 包装法  → AT_PACK_POA.docx                             │
│     - 电池法  → AT_Battery_POA.docx                          │
│     - WEEE    → AT_WEEE_POA.docx                             │
│                                                              │
│  3. 替换模板占位符（5 个字段，见 3.2 节）                      │
│                                                              │
│  4. Word 转 PDF（convertWordToPdf）                          │
│                                                              │
│  5. 上传 OSS → AT_OSS_EPR_FILE_PATH                         │
│                                                              │
│  6. 写入本地表（拟 pack_at_register）                         │
│     - Power_of_attorney ← OSS URL                            │
│                                                              │
│  7. 更新 EPRRegInfo.PushTaxBureauStatus = 2（待推送）         │
└──────────────────────────────────────────────────────────────┘
```

> 与比利时流程的区别：比利时是「外部已生成文件 → 从 Base_AnnexesFile 取路径」；
> 奥地利改为「本任务内直接生成 → 上传 OSS → 拿到 URL 入库」，不再依赖 Base_AnnexesFile。

---

## 3. 模板与字段映射

### 3.1 模板选择（按 TypeName）

| rt.TypeName | 模板文件（resources/template/） | 说明 |
|-------------|-------------------------------|------|
| 包装法 | `AT_PACK_POA.docx` | 奥地利包装法授权书 |
| 电池法 | `AT_Battery_POA.docx` | 奥地利电池法授权书 |
| WEEE | `AT_WEEE_POA.docx` | 奥地利 WEEE 授权书 |

### 3.2 模板占位符与字段映射（用户确认）

三个 AT 模板的占位符完全一致，共 5 个：

| 占位符 | 字段说明 | 对应数据库字段 | 示例值 | 备注 |
|--------|----------|----------------|--------|------|
| `{{NameEng}}` | 公司英文名 | `Base_Customer_Company.NameEng` | jiujiangyupuqunmaoyiyouxiangongsi | 按客户后台/表格字段，不要直接翻译 |
| `{{RegAddress}}` | 地址 | `Base_Customer_Company.RegAddress` | jiangxisheng jiujiangshi deanxian gaoxinjishuchanyeyuankechuangzhongxindalou 09003shi-jiB00480 | 地址内不要包含邮编；按大小顺序排列 |
| `{{CompanyAddressPostcode}}` | 邮编 | `Base_Customer_Company.CompanyAddressPostcode` | 330499 | 城市对应的邮编 |
| `{{CountryTwoCode}}` | 国家代码 | **见下方说明** | CN | 公司属地，对应的国际英文代码 |
| `{{LegalPersonFullNamePinYin}}` | 法人名（名前姓后加空格） | `Base_Customer_Company.LegalPersonFullNamePinYin` | | |

> **⚠️ 关于国家代码（需确认）**：模板占位符是 `{{CountryTwoCode}}`。
> 现有 SQL 通过 `LEFT JOIN Country c ON c.CountryName = bc.Country` 取 `c.CountryTwoCode` 得到 ISO 二字码（如 CN）。
> 您给的映射是 `Base_Customer_Company.Country`。需确认 `bc.Country` 存的是国家**名称**还是**二字码**：
> - 若存二字码 → 直接用 `bc.Country`；
> - 若存国家名 → 保留 `Country` 表 JOIN 取 `c.CountryTwoCode`。

### 3.3 模板占位符核对结果

已实际解析三个 docx 模板，占位符如下（均为文本替换，无签名图片占位符）：

| 模板 | 占位符 |
|------|--------|
| AT_PACK_POA.docx | `{{NameEng}} {{RegAddress}} {{CompanyAddressPostcode}} {{CountryTwoCode}} {{LegalPersonFullNamePinYin}}` |
| AT_Battery_POA.docx | 同上 5 个 |
| AT_WEEE_POA.docx | 同上 5 个 |

> 模板中**没有** `{{date}}`、`{{LegalPersonSignature}}` 占位符，因此：
> - 不需要日期替换；
> - 不需要签名图片嵌入（与 BE 不同，BE 模板有 `{{LegalPersonSignature}}`）。
> 如需加签名/日期，需要先改模板，请在第 9 节确认。

---

## 4. 数据库设计

### 4.1 SaaS 端查询（在现有 SQL 上修改）

现有 `processBePackData()` 的 SQL 已经包含三种 TypeName，但 **SELECT 没有返回 `rt.TypeName`**，需加上，否则无法选择模板。

修改后的 SQL：

```sql
SELECT TOP 10
    br.BusinessSerialNumber, br.ID AS RegisterID, ei.ID AS EPRRegInfoID, br.BusinessCode,
    br.VATNumber, gt.BusinessLicenseDirection, gt.ProductsRange_En,
    rt.TypeName,                       -- ← 新增：用于选择模板
    bc.*, c.IsEUMember, c.CountryTwoCode, c.CountryName_en
FROM EPRBusinessRecord br
LEFT JOIN EPRRegInfo ei ON br.ID = ei.EPRBusinessRecordId
LEFT JOIN GeneralTemplateEPR gt ON gt.ID = br.ID
LEFT JOIN Base_Customer_Company bc ON bc.ID = br.CompanyId
LEFT JOIN RecyclingType rt ON rt.ID = br.RecyclingTypeId
LEFT JOIN Country c ON c.CountryName = bc.Country
LEFT JOIN ServiceItems si ON br.ServiceItemId = si.ID
WHERE br.Country = 'AT' AND ei.PushTaxBureauStatus = 1 AND ei.PushType = '301'
  AND (rt.TypeName = '包装法' OR rt.TypeName = 'WEEE' OR rt.TypeName = '电池法')
```

> 若确认使用 `bc.Country` 作为国家代码，可去掉 `Country` 表 JOIN，直接读 `bc.Country`（需确认字段内容）。

### 4.2 本地目标表（拟新建 `pack_at_register`，待确认）

目前 `at_epr_pack_auto_file.php` 写的是比利时表 `pack_be_register`。奥地利建议新建独立表（复制 `doc/自动化表.txt` 中 `pack_be_register` 的结构），并做两点调整：

1. 增加 `TypeName NVARCHAR(50)`（包装法/电池法/WEEE）；
2. 奥地利每条记录只生成 **1 个授权书文件**，只需 `Power_of_attorney` 字段，`Membership_agreement` 可保留置空或删除。

> 是否沿用 `pack_be_register` 还是新建 `pack_at_register`，请在 9 节确认。

---

## 5. 代码修改方案（job/at_epr_pack_auto_file.php）

### 5.1 整体改动点

| 位置 | 现状 | 修改 |
|------|------|------|
| `processBePackData()` SQL | 未返回 `rt.TypeName` | SELECT 增加 `rt.TypeName` |
| `processBePackData()` 主流程 | 从 `Base_AnnexesFile` 取路径（`getFilePathsFromAnnexes`） | 改为「选模板 → 生成 PDF → 上传 OSS → 取 URL」 |
| `validateData()` | 校验 `CompanyAddressLine1En` / `CityEngName` 等 | 改为校验 AT 模板需要的字段：`NameEng`、`RegAddress`、`CompanyAddressPostcode`、`LegalPersonFullNamePinYin`、`TypeName`、国家代码 |
| `insertOrUpdateRegistrationInfo()` | 写入 `pack_be_register`，含 Mandat + Membership | 写入 `pack_at_register`，仅授权书 URL；新增 `TypeName` 字段 |
| `getFilePathsFromAnnexes()` | 读取 Base_AnnexesFile | 删除（不再依赖） |

### 5.2 新增方法：模板选择

```php
/**
 * 根据 TypeName 选择奥地利 EPR 授权书模板
 * @param string $typeName rt.TypeName 值
 * @return string 模板文件绝对路径
 */
private function getAtTemplatePath($typeName) {
    $templateMap = [
        '包装法' => 'AT_PACK_POA.docx',
        '电池法' => 'AT_Battery_POA.docx',
        'WEEE'   => 'AT_WEEE_POA.docx'
    ];
    $fileName = $templateMap[$typeName] ?? null;
    if (empty($fileName)) {
        throw new Exception("未识别的奥地利 EPR 类型: " . $typeName);
    }
    return $this->downloadBaseDir . '/template/' . $fileName;
}
```

### 5.3 新增方法：生成授权书 PDF

复用 `public/function.php` 中的 `prepareAndProcessWordDocument()`、`replaceWordContent()`、`convertWordToPdf()`：

```php
/**
 * 生成奥地利 EPR 授权书 PDF
 * @param array $dataRow 业务数据（含 bc.* 字段与 rt.TypeName）
 * @param string $recordDir 记录目录（resources/AT_EPRfile/{BusinessSerialNumber}）
 * @param string $templatePath 模板路径
 * @return array ['success'=>bool, 'path'=>string, 'msg'=>string]
 */
private function generateAtPoa($dataRow, $recordDir, $templatePath) {
    $result = ['success' => false, 'path' => '', 'msg' => ''];
    $safeFileName = "POA_{$dataRow['TypeName']}_{$dataRow['NameEng']}.pdf";
    $paths = [
        'template'     => $templatePath,
        'output'       => "$recordDir/AT_POA_Output.docx",
        'wordToPdf'    => "$recordDir/$safeFileName"
    ];
    try {
        $prepareResult = prepareAndProcessWordDocument($paths, function ($documentXml) use ($dataRow) {
            return replaceWordContent($documentXml, [
                '{{NameEng}}'                  => trim($dataRow['NameEng'] ?? ''),
                '{{RegAddress}}'               => trim($dataRow['RegAddress'] ?? ''),
                '{{CompanyAddressPostcode}}'   => trim($dataRow['CompanyAddressPostcode'] ?? ''),
                '{{CountryTwoCode}}'           => trim($dataRow['CountryTwoCode'] ?? ''),
                '{{LegalPersonFullNamePinYin}}'=> trim($dataRow['LegalPersonFullNamePinYin'] ?? '')
            ]);
        });
        if (!$prepareResult['success']) {
            return $prepareResult;
        }
        $pdfResult = convertWordToPdf($paths['output'], $paths['wordToPdf']);
        if (!$pdfResult['success']) {
            return $pdfResult;
        }
        $result['success'] = true;
        $result['path'] = $paths['wordToPdf'];
    } catch (Exception $e) {
        $result['msg'] = '生成奥地利授权书异常: ' . $e->getMessage();
    }
    return $result;
}
```

### 5.4 上传 OSS

```php
// 与 BE 相同，仅 OSS 路径换成 AT_OSS_EPR_FILE_PATH
$localName = basename($poaResult['path']);
$ossResult = HttpCosUploader::upload($poaResult['path'], $localName, AT_OSS_EPR_FILE_PATH);
if (!$ossResult['success']) {
    throw new Exception('授权书上传 OSS 失败: ' . $ossResult['message']);
}
$poaUrl = $ossResult['url'];
```

### 5.5 主流程调整（processBePackData 内）

```
foreach ($results as $dataRow) {
    1. 本地表查重（pack_at_register WHERE tid=...，status=2 则跳过）
    2. validateData($dataRow)                     // 按 AT 字段校验
    3. $recordDir = resources/AT_EPRfile/{BusinessSerialNumber}，不存在则创建
    4. $templatePath = getAtTemplatePath($dataRow['TypeName'])
    5. $poaResult = generateAtPoa($dataRow, $recordDir, $templatePath)
       失败 → updateEPRRegInfoStatus(-1) + cleanRecordDirectory，continue
    6. HttpCosUploader::upload(..., AT_OSS_EPR_FILE_PATH)
       失败 → updateEPRRegInfoStatus(-1) + cleanRecordDirectory，continue
    7. insertOrUpdateRegistrationInfo($dataRow, $poaUrl, $existing)   // 写 pack_at_register
    8. updateEPRRegInfoStatus(EPRRegInfoID, 2, '')                    // 待推送
    9. cleanRecordDirectory($recordDir)                                // 清理本地临时文件
}
```

---

## 6. 状态流转（沿用比利时）

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

| EPRRegInfo.PushTaxBureauStatus | 含义 | 触发条件 |
|----|------|----------|
| 1 | 待生成文件 | 初始状态 |
| 2 | 待推送官方 | 授权书生成并上传 OSS 成功后 |
| 3 | 推送完成 | 后续推送流程（本期不涉及） |
| -1 | 失败 | 字段缺失 / 生成失败 / OSS 上传失败 |

---

## 7. 错误处理

| 场景 | 处理策略 |
|------|----------|
| 未查询到待处理数据 | 正常跳过，等待下次 |
| 字段缺失（NameEng/RegAddress/邮编/法人名/TypeName 等） | 更新 EPRRegInfo.status=-1，记录缺失字段，continue |
| TypeName 无法匹配模板 | 更新 status=-1，记录错误 |
| Word 替换 / 转 PDF 失败 | 更新 status=-1，清理临时目录 |
| OSS 上传失败 | 更新 status=-1，清理临时目录 |
| 本地表写入失败 | 更新 status=-1，记录错误 |

---

## 8. 开发步骤

| 序号 | 任务 |
|------|------|
| 1 | 确认待确认问题（第 9 节） |
| 2 | 修改 `processBePackData()` SQL，返回 `rt.TypeName` |
| 3 | 新增 `getAtTemplatePath()` / `generateAtPoa()` 方法 |
| 4 | 调整 `validateData()` 为 AT 所需字段 |
| 5 | 调整入库逻辑为 `pack_at_register`（或确认沿用旧表），写入 OSS URL |
| 6 | 接入 `AT_OSS_EPR_FILE_PATH` 上传 |
| 7 | 用一条测试数据（PushTaxBureauStatus=1 的 AT 记录）跑通全流程 |
| 8 | 核对生成 PDF 内容与模板一致，检查 OSS 文件可访问 |

---

## 9. 待确认问题

| # | 问题 | 影响 |
|---|------|------|
| 1 | 本地表：新建 `pack_at_register`，还是继续用 `pack_be_register`？ | 影响入库逻辑与建表脚本 |
| 2 | 国家代码：`Base_Customer_Company.Country` 存的是二字码（如 CN）还是国家名？模板占位符是 `{{CountryTwoCode}}` | 决定是否保留 Country 表 JOIN |
| 3 | AT 模板无签名/日期占位符，授权书是否需要签名图片或日期？若需要，需先改模板再开发 | 影响生成流程复杂度 |
| 4 | 是否还需要把生成的授权书记录写入 `Base_AnnexesFile`（供 SaaS 前端展示）？ | 影响是否新增写附件表逻辑 |
| 5 | `PushType = '301'` 对奥地利三种类型是否都适用？ | 影响查询结果范围 |
| 6 | 授权书 PDF 命名规则是否按 `POA_{TypeName}_{NameEng}.pdf`？ | 影响文件名与 OSS 路径可读性 |

---

## 10. 日志

沿用 `public/function.php` 的 `logMessage()`，日志在 `vat_api_se_client/logs/`。

示例：
```
[2026-08-18 16:41:00] 找到 3 条奥地利EPR注册数据需要处理
[2026-08-18 16:41:02] 生成授权书成功: AT-xxx 类型=包装法 OSS=https://file.usaeu.com/vat_factory/at_epr_file/POA_包装法_xxx.pdf
[2026-08-18 16:41:03] 更新 EPRRegInfo.PushTaxBureauStatus=2 ID=123
```

---

**文档版本**: v0.1（待评审）
**最后更新**: 2026-08-18
