# 开发者指南

## 概述

本文档为开发者提供详细的指导，说明如何扩展应用撤回接口系统。通过本指南，您将学会如何添加新的国家类、实现新的应用方法，以及遵循系统的最佳实践和规范。

## 系统架构概览

系统采用面向对象的设计模式，核心组件包括：

- **API 入口** (`index.php`): 接收请求并路由到相应的处理类
- **数据库管理** (`core/Database.php`): 单例模式管理数据库连接
- **响应处理** (`core/Response.php`): 统一的 JSON 响应格式
- **国家基类** (`core/CountryBase.php`): 提供数据库连接和通用方法
- **国家类** (`countries/*.php`): 实现特定国家的应用处理逻辑

### 目录结构

```
project/
├── index.php                 # API 入口文件
├── config/
│   └── database.php         # 数据库配置
├── core/
│   ├── Database.php         # 数据库管理类
│   ├── Response.php         # 响应处理类
│   └── CountryBase.php      # 国家处理基类
├── countries/
│   ├── de.php              # 德国处理类
│   ├── fr.php              # 法国处理类
│   ├── it.php              # 意大利处理类
│   ├── gb.php              # 英国处理类
│   ├── be.php              # 比利时处理类
│   └── es.php              # 西班牙处理类
└── docs/
    ├── API_USAGE.md        # API 使用文档
    └── DEVELOPER_GUIDE.md  # 本文档
```

## 添加新国家类

### 步骤 1: 创建国家类文件

在 `countries/` 目录下创建新的 PHP 文件，文件名使用国家代码（小写）。

**命名规范**:
- 文件名: `{国家代码}.php`（例如: `us.php`, `cn.php`, `de.php`）
- 类名: 与文件名相同（例如: `us`, `cn`, `de`）


**示例**: 创建美国（US）国家类

```bash
# 创建文件
touch countries/us.php
```

### 步骤 2: 编写国家类代码

国家类必须继承 `CountryBase` 基类，以获得数据库连接和通用方法。

**基本模板**:

```php
<?php

/**
 * {国家名称}国家处理类
 * 
 * 处理{国家名称}相关的应用撤回请求
 */
class {国家代码} extends CountryBase
{
    /**
     * 构造函数（可选）
     * 
     * 如果需要特殊的初始化逻辑，可以重写构造函数
     * 但必须调用父类构造函数以初始化数据库连接
     */
    public function __construct()
    {
        parent::__construct();
        // 添加特定的初始化逻辑
    }
    
    // 在这里添加应用方法...
}
```

**完整示例**: 美国（US）国家类

```php
<?php

/**
 * us - 美国国家处理类
 * 
 * 处理美国相关的应用撤回请求
 */
class us extends CountryBase
{
    /**
     * 处理美国税号申请（Tax ID Application）撤回请求
     * 
     * @param string $id 记录的唯一标识符
     * @param string $code 流水号
     * @return array 返回标准格式的结果数组
     */
    public function taxid(array $requiredParams): array
    {
        $businessSerialNumber = $requiredParams['BusinessSerialNumber'];

        // 先查询当前状态
        $checkSql = "SELECT application_status FROM us_tax_applications WHERE application_id = ?";
        $checkResult = $this->fetchOne($checkSql, [$businessSerialNumber]);

        if ($checkResult['code'] !== 200) {
            return [
                'code' => 400,
                'msg' => '未找到指定的税号申请记录',
                'data' => null
            ];
        }

        $currentStatus = $checkResult['data']['application_status'];
        if ($currentStatus != 0 && $currentStatus != -1) {
            return [
                'code' => 400,
                'msg' => '该记录当前状态不允许撤回',
                'data' => null
            ];
        }

        $sql = "UPDATE us_tax_applications 
                SET application_status = ? 
                WHERE application_id = ?";
        
        $params = [5, $businessSerialNumber];
        
        $result = $this->execute($sql, $params);
        
        if ($result['code'] === 200 && ($result['data']['affected_rows'] ?? 0) > 0) {
            return [
                'code' => 200,
                'msg' => '税号申请撤回成功',
                'data' => null
            ];
        }
        
        return [
            'code' => 400,
            'msg' => '未找到指定的税号申请记录',
            'data' => null
        ];
    }
}
```

### 步骤 3: 测试国家类

创建国家类后，使用 API 进行测试：

```bash
curl -X POST http://your-domain.com/index.php \
  -d "country=us" \
  -d "app_name=taxid" \
  -d "id=test-id-123" \
  -d "code=test-code-456"
```


## 添加新应用方法

### 步骤 1: 确定应用需求

在添加新应用方法之前，明确以下信息：

1. **应用名称**: 方法名（例如: `vatreg`, `taxid`, `license`）
2. **数据库表**: 需要操作的表名
3. **匹配字段**: 用于定位记录的字段（例如: `source_record_id`, `application_id`）
4. **状态字段**: 需要更新的状态字段（例如: `registration_status`, `application_status`）
5. **撤回状态值**: 表示已撤回的状态值（例如: `5`, `'revoked'`, `'cancelled'`）

### 步骤 2: 在国家类中添加方法

在对应的国家类中添加新的公共方法。

**方法签名规范**:

```php
public function {应用名称}(array $requiredParams): array
```

- 方法名: 使用小写字母和下划线，对应 `{pushScene}_{pushType}` 的拼接结果
- 参数: 接收 `$requiredParams` 关联数组，包含以下键：
  - `BusinessId` - 业务记录 ID
  - `BusinessSerialNumber` - 业务流水号
  - `PushId` - 推送记录 ID
  - `PushScene` - 推送场景
  - `PushType` - 推送类型
  - `PushTaxBureauStatus` - 推送税局状态
  - `Country` - 国家代码
- **返回值**: 必须返回包含 `code`、`msg` 和 `data` 的数组

**状态预校验**:

在执行 UPDATE 撤回操作前，必须先查询记录当前状态，**仅当 status 为 0（待处理）或 -1（处理中）时才允许撤回**。使用基类提供的 `fetchOne()` 方法进行查询。

**基本模板**:

```php
/**
 * 处理{应用描述}撤回请求
 * 
 * @param array $requiredParams 请求参数数组
 * @return array 返回标准格式的结果数组（code, msg, data）
 */
public function {应用名称}($requiredParams): array
{
    $businessSerialNumber = $requiredParams['BusinessSerialNumber'];

    // 1. 先查询当前状态，仅 status=0 或 status=-1 才允许撤回
    $checkSql = "SELECT status FROM {表名} WHERE {匹配字段} = ?";
    $checkResult = $this->fetchOne($checkSql, [$businessSerialNumber]);

    if ($checkResult['code'] !== 200) {
        return [
            'code' => 400,
            'msg' => '未找到指定的记录',
            'data' => null
        ];
    }

    $currentStatus = $checkResult['data']['status'];
    if ($currentStatus != 0 && $currentStatus != -1) {
        return [
            'code' => 400,
            'msg' => '该记录当前状态不允许撤回',
            'data' => null
        ];
    }

    // 2. 构造 SQL UPDATE 语句
    $sql = "UPDATE {表名} 
            SET {状态字段} = ? 
            WHERE {匹配字段} = ?";
    
    // 3. 准备参数数组
    $params = [5, $businessSerialNumber];
    
    // 4. 执行 SQL 更新操作
    $result = $this->execute($sql, $params);
    
    // 5. 检查更新结果
    if ($result['code'] === 200) {
        $affectedRows = $result['data']['affected_rows'] ?? 0;
        
        if ($affectedRows > 0) {
            // 更新成功
            return [
                'code' => 200,
                'msg' => '{应用描述}撤回成功',
                'data' => null
            ];
        } else {
            // 没有找到匹配的记录
            return [
                'code' => 400,
                'msg' => '未找到指定的{应用描述}记录',
                'data' => null
            ];
        }
    }
    
    // 数据库操作失败
    return [
        'code' => 400,
        'msg' => '{应用描述}撤回失败: ' . $result['msg'],
        'data' => null
    ];
}
```


### 步骤 3: 完整示例

**示例 1**: 德国 VAT 注册撤回（已实现，带状态预校验）

```php
public function vatpushtax_101(array $requiredParams): array
{
    $businessSerialNumber = $requiredParams['BusinessSerialNumber'];

    // 先查询当前状态
    $checkSql = "SELECT status FROM vat_de_register WHERE order_serial_number = ?";
    $checkResult = $this->fetchOne($checkSql, [$businessSerialNumber]);

    if ($checkResult['code'] !== 200) {
        return [
            'code' => 400,
            'msg' => '未找到指定的德国VAT注册记录',
            'data' => null
        ];
    }

    $currentStatus = $checkResult['data']['status'];
    if ($currentStatus != 0 && $currentStatus != -1) {
        return [
            'code' => 400,
            'msg' => '该记录当前状态不允许撤回',
            'data' => null
        ];
    }

    $sql = "UPDATE vat_de_register SET status = ? WHERE order_serial_number = ?";
    $params = [5, $businessSerialNumber];
    $result = $this->execute($sql, $params);

    if ($result['code'] === 200) {
        $affectedRows = $result['data']['affected_rows'] ?? 0;
        if ($affectedRows > 0) {
            return [
                'code' => 200,
                'msg' => '德国VAT注册撤回成功',
                'data' => null
            ];
        } else {
            return [
                'code' => 400,
                'msg' => '未找到指定的德国VAT注册记录',
                'data' => null
            ];
        }
    }

    return [
        'code' => 400,
        'msg' => '德国VAT注册撤回失败: ' . $result['msg'],
        'data' => null
    ];
}
```

**示例 2**: 法国 VAT 注册撤回（已实现，带状态预校验，允许 status=-1）

```php
public function vatpushtax_101(array $requiredParams): array
{
    $businessSerialNumber = $requiredParams['BusinessSerialNumber'];

    // 先查询当前状态
    $checkSql = "SELECT status FROM vat_fr_register WHERE tid = ?";
    $checkResult = $this->fetchOne($checkSql, [$businessSerialNumber]);

    if ($checkResult['code'] !== 200) {
        return [
            'code' => 400,
            'msg' => '未找到指定的法国VAT注册记录',
            'data' => null
        ];
    }

    $currentStatus = $checkResult['data']['status'];
    if ($currentStatus != 0 && $currentStatus != -1) {
        return [
            'code' => 400,
            'msg' => '该记录当前状态不允许撤回',
            'data' => null
        ];
    }

    $sql = "UPDATE vat_fr_register SET status = ? WHERE tid = ?";
    $params = [5, $businessSerialNumber];
    $result = $this->execute($sql, $params);

    // ... 后续结果处理同上
}
```

### 步骤 4: 测试应用方法

```bash
# 测试新添加的应用方法
curl -X POST http://your-domain.com/app_withdrawn/index.php \
  -d "BusinessId=abc-123" \
  -d "BusinessSerialNumber=TEST2024001" \
  -d "PushId=push-001" \
  -d "PushScene=vatpushtax" \
  -d "PushType=101" \
  -d "PushTaxBureauStatus=1" \
  -d "Country=DE"
```


## 数据库操作最佳实践

### 1. 使用参数化查询

**始终使用参数化查询防止 SQL 注入**。`CountryBase` 类的 `execute()` 方法已经实现了参数化查询。

**正确示例**:

```php
// ✅ 正确：使用参数化查询
$sql = "UPDATE users SET status = ? WHERE user_id = ?";
$params = ['inactive', $userId];
$result = $this->execute($sql, $params);
```

**错误示例**:

```php
// ❌ 错误：直接拼接 SQL（存在 SQL 注入风险）
$sql = "UPDATE users SET status = 'inactive' WHERE user_id = '$userId'";
$result = $this->execute($sql, []);
```

### 2. 使用 `fetchOne()` 查询记录状态

在执行撤回操作前，必须使用 `fetchOne()` 方法查询当前记录状态，仅在允许状态下执行 UPDATE：

```php
// 查询当前状态
$checkSql = "SELECT status FROM target_table WHERE tid = ?";
$checkResult = $this->fetchOne($checkSql, [$businessSerialNumber]);

if ($checkResult['code'] !== 200) {
    // 记录不存在
    return [
        'code' => 400,
        'msg' => '未找到指定的记录',
        'data' => null
    ];
}

// 仅 status=0（待处理）或 status=-1（处理中）允许撤回
$currentStatus = $checkResult['data']['status'];
if ($currentStatus != 0 && $currentStatus != -1) {
    return [
        'code' => 400,
        'msg' => '该记录当前状态不允许撤回',
        'data' => null
    ];
}
```

### 3. 使用命名参数（可选）

PDO 支持命名参数，可以提高代码可读性：

```php
$sql = "UPDATE uk_vat_register 
        SET registration_status = :status, 
            updated_at = GETDATE() 
        WHERE source_record_id = :id 
        AND code = :code";

$params = [
    ':status' => 5,
    ':id' => $id,
    ':code' => $code
];

$result = $this->execute($sql, $params);
```

### 3. 检查影响行数

始终检查 `affected_rows` 以确认操作是否成功：

```php
$result = $this->execute($sql, $params);

if ($result['code'] === 200) {
    $affectedRows = $result['data']['affected_rows'] ?? 0;
    
    if ($affectedRows > 0) {
        // 更新成功
    } else {
        // 没有找到匹配的记录
    }
}
```

### 4. 事务处理（高级）

对于需要多个数据库操作的复杂场景，使用事务确保数据一致性：

```php
public function complex_operation(string $id, string $code): array
{
    try {
        // 开始事务
        $this->db->beginTransaction();
        
        // 第一个操作
        $sql1 = "UPDATE table1 SET status = ? WHERE id = ?";
        $stmt1 = $this->db->prepare($sql1);
        $stmt1->execute([5, $id]);
        
        // 第二个操作
        $sql2 = "INSERT INTO audit_log (record_id, action, timestamp) VALUES (?, ?, GETDATE())";
        $stmt2 = $this->db->prepare($sql2);
        $stmt2->execute([$id, 'revoked']);
        
        // 提交事务
        $this->db->commit();
        
        return [
            'code' => 200,
            'msg' => '操作成功',
            'data' => null
        ];
        
    } catch (PDOException $e) {
        // 回滚事务
        $this->db->rollBack();
        
        return [
            'code' => 400,
            'msg' => '操作失败: ' . $e->getMessage(),
            'data' => null
        ];
    }
}
```

### 5. 数据库连接管理

- 数据库连接由 `Database` 类的单例模式管理
- 不要手动创建新的数据库连接
- 连接使用持久化模式（`PDO::ATTR_PERSISTENT`）以提高性能

```php
// ✅ 正确：使用继承的数据库连接
$this->db->prepare($sql);

// ❌ 错误：不要手动创建连接
$newDb = new PDO($dsn, $user, $pass);
```


## 错误处理和返回格式规范

### 标准返回格式

所有应用方法必须返回包含以下字段的数组：

```php
[
    'code' => int,      // 状态码：200=成功，400=失败
    'msg' => string,    // 消息描述
    'data' => mixed     // 返回数据（可选，可以是数组、null 等）
]
```

### 成功响应规范

**基本成功响应**:

```php
return [
    'code' => 200,
    'msg' => '操作成功的描述',
    'data' => null
];
```

**包含数据的成功响应**:

```php
return [
    'code' => 200,
    'msg' => '撤回成功',
    'data' => null
];
```

### 失败响应规范

**记录不存在**:

```php
return [
    'code' => 400,
    'msg' => '未找到指定的记录',
    'data' => null
];
```

**记录状态不允许撤回**:

```php
return [
    'code' => 400,
    'msg' => '该记录当前状态不允许撤回',
    'data' => null
];
```

**数据库操作失败**:

```php
return [
    'code' => 400,
    'msg' => '操作失败: ' . $result['msg'],
    'data' => null
];
```

**参数验证失败**:

```php
return [
    'code' => 400,
    'msg' => '参数验证失败: BusinessSerialNumber 不能为空',
    'data' => null
];
```

### 错误处理最佳实践

#### 1. 捕获异常

始终使用 try-catch 捕获可能的异常：

```php
public function myapp(string $id, string $code): array
{
    try {
        // 业务逻辑
        $result = $this->execute($sql, $params);
        
        // 处理结果
        return [
            'code' => 200,
            'msg' => '操作成功',
            'data' => null
        ];
        
    } catch (Exception $e) {
        // 记录错误日志（可选）
        error_log("Error in myapp: " . $e->getMessage());
        
        return [
            'code' => 400,
            'msg' => '系统错误，请稍后重试',
            'data' => null
        ];
    }
}
```

#### 2. 提供有意义的错误消息

错误消息应该清晰、具体，但不暴露敏感信息：

```php
// ✅ 正确：清晰且安全的错误消息
'msg' => '未找到指定的增值税注册记录'
'msg' => '数据库操作失败，请稍后重试'
'msg' => '参数验证失败: ID 不能为空'

// ❌ 错误：暴露敏感信息
'msg' => 'SQL Error: Table uk_vat_register not found'
'msg' => 'PDOException: SQLSTATE[42S02] at line 45'
```

#### 3. 验证输入参数

在执行数据库操作前验证参数：

```php
public function myapp(string $id, string $code): array
{
    // 验证 ID 格式
    if (empty($id) || strlen($id) > 100) {
        return [
            'code' => 400,
            'msg' => '参数验证失败: ID 格式不正确',
            'data' => null
        ];
    }
    
    // 验证 code 格式
    if (empty($code) || !preg_match('/^[A-Za-z0-9]+$/', $code)) {
        return [
            'code' => 400,
            'msg' => '参数验证失败: 流水号格式不正确',
            'data' => null
        ];
    }
    
    // 继续执行业务逻辑...
}
```


#### 4. 日志记录（可选）

对于生产环境，建议记录错误日志：

```php
public function myapp(string $id, string $code): array
{
    try {
        $result = $this->execute($sql, $params);
        
        if ($result['code'] !== 200) {
            // 记录错误日志
            error_log(sprintf(
                "[%s] Database error in myapp: %s (ID: %s, Code: %s)",
                date('Y-m-d H:i:s'),
                $result['msg'],
                $id,
                $code
            ));
        }
        
        return $result;
        
    } catch (Exception $e) {
        // 记录异常日志
        error_log(sprintf(
            "[%s] Exception in myapp: %s (ID: %s, Code: %s)",
            date('Y-m-d H:i:s'),
            $e->getMessage(),
            $id,
            $code
        ));
        
        return [
            'code' => 400,
            'msg' => '系统错误，请稍后重试',
            'data' => null
        ];
    }
}
```

## 完整代码示例

### 示例 1: 带状态预校验的单表更新

```php
<?php

/**
 * de - 德国国家处理类
 */
class de extends CountryBase
{
    /**
     * 处理德国 VAT 注册撤回请求
     * 
     * @param array $requiredParams 请求参数数组
     * @return array 返回标准格式的结果数组
     */
    public function business_reg($requiredParams): array
    {
        $businessSerialNumber = $requiredParams['BusinessSerialNumber'];

        // 先查询当前状态，仅 status=0 或 status=-1 才允许撤回
        $checkSql = "SELECT status FROM de_business_registrations WHERE registration_id = ?";
        $checkResult = $this->fetchOne($checkSql, [$businessSerialNumber]);

        if ($checkResult['code'] !== 200) {
            return [
                'code' => 400,
                'msg' => '未找到指定的记录',
                'data' => null
            ];
        }

        $currentStatus = $checkResult['data']['status'];
        if ($currentStatus != 0 && $currentStatus != -1) {
            return [
                'code' => 400,
                'msg' => '该记录当前状态不允许撤回',
                'data' => null
            ];
        }

        // 构造 SQL 更新语句
        $sql = "UPDATE de_business_registrations 
                SET registration_status = ?, 
                    revoked_at = GETDATE() 
                WHERE registration_id = ?";
        
        $params = [5, $businessSerialNumber];
        
        // 执行数据库操作
        $result = $this->execute($sql, $params);
        
        // 处理结果
        if ($result['code'] === 200) {
            $affectedRows = $result['data']['affected_rows'] ?? 0;
            
            if ($affectedRows > 0) {
                return [
                    'code' => 200,
                    'msg' => '商业注册撤回成功',
                    'data' => null
                ];
            } else {
                return [
                    'code' => 400,
                    'msg' => '未找到指定的商业注册记录',
                    'data' => null
                ];
            }
        }
        
        return [
            'code' => 400,
            'msg' => '商业注册撤回失败: ' . $result['msg'],
            'data' => null
        ];
    }
}
```


### 示例 2: 带事务的复杂操作

```php
<?php

/**
 * fr - 法国国家处理类
 */
class fr extends CountryBase
{
    /**
     * 处理法国公司注册撤回请求（包含审计日志）
     * 
     * @param array $requiredParams 请求参数数组
     * @return array 返回标准格式的结果数组
     */
    public function company_reg($requiredParams): array
    {
        $businessSerialNumber = $requiredParams['BusinessSerialNumber'];
        try {
            // 开始事务
            $this->db->beginTransaction();
            
            // 1. 更新公司注册状态
            $sql1 = "UPDATE fr_company_registrations 
                     SET status = ?, 
                         updated_at = GETDATE() 
                     WHERE company_id = ?";
            
            $stmt1 = $this->db->prepare($sql1);
            $result1 = $stmt1->execute(['cancelled', $id]);
            
            if (!$result1 || $stmt1->rowCount() === 0) {
                $this->db->rollBack();
                return [
                    'code' => 400,
                    'msg' => '未找到指定的公司注册记录',
                    'data' => null
                ];
            }
            
            // 2. 插入审计日志
            $sql2 = "INSERT INTO fr_audit_logs 
                     (record_id, record_type, action, code, created_at) 
                     VALUES (?, ?, ?, ?, GETDATE())";
            
            $stmt2 = $this->db->prepare($sql2);
            $stmt2->execute([$id, 'company_registration', 'revoked', $code]);
            
            // 3. 更新相关文档状态
            $sql3 = "UPDATE fr_documents 
                     SET status = ? 
                     WHERE company_id = ? AND status = ?";
            
            $stmt3 = $this->db->prepare($sql3);
            $stmt3->execute(['archived', $id, 'active']);
            
            // 提交事务
            $this->db->commit();
            
            return [
                'code' => 200,
                'msg' => '公司注册撤回成功',
                'data' => null
            ];
            
        } catch (PDOException $e) {
            // 回滚事务
            if ($this->db->inTransaction()) {
                $this->db->rollBack();
            }
            
            // 记录错误日志
            error_log("Error in fr::company_reg: " . $e->getMessage());
            
            return [
                'code' => 400,
                'msg' => '公司注册撤回失败，请稍后重试',
                'data' => null
            ];
        }
    }
}
```

### 示例 3: 带参数验证的完整实现

```php
<?php

/**
 * jp - 日本国家处理类
 */
class jp extends CountryBase
{
    /**
     * 处理日本营业许可撤回请求
     * 
     * @param string $id 记录的唯一标识符
     * @param string $code 流水号
     * @return array 返回标准格式的结果数组
     */
    public function business_permit(string $id, string $code): array
    {
        // 1. 参数验证
        $validation = $this->validateParams($id, $code);
        if ($validation !== true) {
            return [
                'code' => 400,
                'msg' => $validation,
                'data' => null
            ];
        }
        
        try {
            // 2. 检查记录是否存在
            $checkSql = "SELECT permit_status FROM jp_business_permits WHERE permit_id = ?";
            $checkStmt = $this->db->prepare($checkSql);
            $checkStmt->execute([$id]);
            $record = $checkStmt->fetch();
            
            if (!$record) {
                return [
                    'code' => 400,
                    'msg' => '未找到指定的营业许可记录',
                    'data' => null
                ];
            }
            
            // 3. 检查当前状态是否允许撤回
            if ($record['permit_status'] === 'revoked') {
                return [
                    'code' => 400,
                    'msg' => '该营业许可已经被撤回',
                    'data' => null
                ];
            }
            
            // 4. 执行撤回操作
            $sql = "UPDATE jp_business_permits 
                    SET permit_status = ?, 
                        revoked_code = ?,
                        revoked_at = GETDATE() 
                    WHERE permit_id = ?";
            
            $params = ['revoked', $code, $id];
            $result = $this->execute($sql, $params);
            
            if ($result['code'] === 200 && ($result['data']['affected_rows'] ?? 0) > 0) {
                return [
                    'code' => 200,
                    'msg' => '营业许可撤回成功',
                    'data' => null
                ];
            }
            
            return [
                'code' => 400,
                'msg' => '营业许可撤回失败: ' . $result['msg'],
                'data' => null
            ];
            
        } catch (PDOException $e) {
            error_log("Error in jp::business_permit: " . $e->getMessage());
            
            return [
                'code' => 400,
                'msg' => '系统错误，请稍后重试',
                'data' => null
            ];
        }
    }
    
    /**
     * 验证输入参数
     * 
     * @param string $id
     * @param string $code
     * @return bool|string 返回 true 或错误消息
     */
    private function validateParams(string $id, string $code)
    {
        // 验证 ID
        if (empty($id)) {
            return '参数验证失败: ID 不能为空';
        }
        
        if (strlen($id) > 50) {
            return '参数验证失败: ID 长度不能超过 50 个字符';
        }
        
        if (!preg_match('/^[A-Za-z0-9\-_]+$/', $id)) {
            return '参数验证失败: ID 只能包含字母、数字、连字符和下划线';
        }
        
        // 验证 code
        if (empty($code)) {
            return '参数验证失败: 流水号不能为空';
        }
        
        if (strlen($code) > 30) {
            return '参数验证失败: 流水号长度不能超过 30 个字符';
        }
        
        return true;
    }
}
```


## 测试和调试

### 本地测试

#### 1. 使用 cURL 测试

```bash
# 测试成功场景
curl -X POST http://localhost:8080/index.php \
  -d "BusinessId=test-001" \
  -d "BusinessSerialNumber=TEST20260407000161" \
  -d "PushId=push-001" \
  -d "PushScene=vatpushtax" \
  -d "PushType=101" \
  -d "Country=DE" \
  -v

# 测试错误场景 - 缺少参数
curl -X POST http://localhost:8080/index.php \
  -d "BusinessId=test-001" \
  -d "Country=DE" \
  -v

# 测试错误场景 - 不存在的国家
curl -X POST http://localhost:8080/index.php \
  -d "BusinessId=test-001" \
  -d "BusinessSerialNumber=TEST001" \
  -d "PushId=push-001" \
  -d "PushScene=vatpushtax" \
  -d "PushType=101" \
  -d "Country=XX" \
  -v

# 测试错误场景 - 不存在的应用
curl -X POST http://localhost:8080/index.php \
  -d "BusinessId=test-001" \
  -d "BusinessSerialNumber=TEST001" \
  -d "PushId=push-001" \
  -d "PushScene=unknown" \
  -d "PushType=999" \
  -d "Country=DE" \
  -v
```

#### 2. 使用 Postman 测试

1. 创建新的 POST 请求
2. URL: `http://localhost:8080/index.php`
3. Body 类型: `x-www-form-urlencoded`
4. 添加参数:
   - BusinessId: test-001
   - BusinessSerialNumber: TEST20260407000161
   - PushId: push-001
   - PushScene: vatpushtax
   - PushType: 101
   - Country: DE
5. 发送请求并查看响应

#### 3. 创建测试脚本

创建 `test.php` 文件进行自动化测试：

```php
<?php

/**
 * 测试脚本
 */

$baseUrl = 'http://localhost:8080/index.php';

// 测试用例
$testCases = [
    [
        'name' => '成功场景 - 德国VAT注册撤回',
        'data' => [
            'BusinessId' => 'test-' . time(),
            'BusinessSerialNumber' => 'POEPR20260407000161',
            'PushId' => 'push-001',
            'PushScene' => 'vatpushtax',
            'PushType' => '101',
            'Country' => 'DE'
        ],
        'expected_code' => 200
    ],
    [
        'name' => '错误场景 - 缺少参数',
        'data' => [
            'BusinessId' => 'test-001',
            'Country' => 'DE'
        ],
        'expected_code' => 400
    ],
    [
        'name' => '错误场景 - 不支持的国家',
        'data' => [
            'BusinessId' => 'test-001',
            'BusinessSerialNumber' => 'TEST001',
            'PushId' => 'push-001',
            'PushScene' => 'vatpushtax',
            'PushType' => '101',
            'Country' => 'XX'
        ],
        'expected_code' => 400
    ],
    [
        'name' => '错误场景 - 不支持的应用',
        'data' => [
            'BusinessId' => 'test-001',
            'BusinessSerialNumber' => 'TEST001',
            'PushId' => 'push-001',
            'PushScene' => 'unknown',
            'PushType' => '999',
            'Country' => 'DE'
        ],
        'expected_code' => 400
    ]
];

// 执行测试
foreach ($testCases as $index => $testCase) {
    echo "\n测试 " . ($index + 1) . ": " . $testCase['name'] . "\n";
    echo str_repeat('-', 60) . "\n";
    
    $options = [
        'http' => [
            'header'  => "Content-type: application/x-www-form-urlencoded\r\n",
            'method'  => 'POST',
            'content' => http_build_query($testCase['data'])
        ]
    ];
    
    $context = stream_context_create($options);
    $result = @file_get_contents($baseUrl, false, $context);
    
    if ($result === false) {
        echo "❌ 请求失败\n";
        continue;
    }
    
    $response = json_decode($result, true);
    
    if ($response['code'] === $testCase['expected_code']) {
        echo "✅ 测试通过\n";
    } else {
        echo "❌ 测试失败\n";
        echo "   期望状态码: " . $testCase['expected_code'] . "\n";
        echo "   实际状态码: " . $response['code'] . "\n";
    }
    
    echo "响应: " . json_encode($response, JSON_UNESCAPED_UNICODE) . "\n";
}

echo "\n" . str_repeat('=', 60) . "\n";
echo "测试完成\n";
```

运行测试脚本：

```bash
php test.php
```

### 调试技巧

#### 1. 启用错误报告

在开发环境的 `index.php` 顶部添加：

```php
<?php
// 仅在开发环境使用
error_reporting(E_ALL);
ini_set('display_errors', 1);
```

#### 2. 添加调试日志

在应用方法中添加调试输出：

```php
public function myapp(string $id, string $code): array
{
    // 调试日志
    error_log("DEBUG: myapp called with id=$id, code=$code");
    
    $result = $this->execute($sql, $params);
    
    // 调试日志
    error_log("DEBUG: execute result: " . json_encode($result));
    
    return $result;
}
```

#### 3. 使用 var_dump 检查变量

```php
// 临时调试代码
var_dump($id, $code, $result);
exit;
```

#### 4. 检查 SQL 语句

```php
// 输出 SQL 语句进行检查
error_log("SQL: $sql");
error_log("Params: " . json_encode($params));
```


## 常见问题和解决方案

### Q1: 如何处理多个数据库连接？

如果不同国家需要连接不同的数据库，可以在国家类中重写构造函数：

```php
class us extends CountryBase
{
    public function __construct()
    {
        // 不调用父类构造函数
        // 使用自定义的数据库配置
        $config = require __DIR__ . '/../config/database_us.php';
        
        $dsn = sprintf("sqlsrv:Server=%s;Database=%s", $config['host'], $config['database']);
        $this->db = new PDO($dsn, $config['username'], $config['password']);
    }
}
```

### Q2: 如何实现软删除而不是更新状态？

修改 SQL 语句以执行软删除：

```php
public function myapp(string $id, string $code): array
{
    $sql = "UPDATE my_table 
            SET deleted_at = GETDATE(), 
                deleted_by = ?,
                is_deleted = 1 
            WHERE record_id = ? AND is_deleted = 0";
    
    $params = ['system', $id];
    
    $result = $this->execute($sql, $params);
    
    // 处理结果...
}
```

### Q3: 如何添加权限验证？

在应用方法中添加权限检查：

```php
public function myapp(string $id, string $code): array
{
    // 权限验证（示例）
    if (!$this->hasPermission($code)) {
        return [
            'code' => 403,
            'msg' => '权限不足',
            'data' => null
        ];
    }
    
    // 继续执行业务逻辑...
}

private function hasPermission(string $code): bool
{
    // 实现权限检查逻辑
    // 例如：查询数据库、验证 token 等
    return true;
}
```

### Q4: 如何处理大批量撤回？

对于批量操作，建议创建专门的批量处理方法：

```php
public function batch_revoke(array $ids, string $code): array
{
    try {
        $this->db->beginTransaction();
        
        $successCount = 0;
        $failedIds = [];
        
        foreach ($ids as $id) {
            $result = $this->myapp($id, $code);
            
            if ($result['code'] === 200) {
                $successCount++;
            } else {
                $failedIds[] = $id;
            }
        }
        
        $this->db->commit();
        
        return [
            'code' => 200,
            'msg' => "批量撤回完成",
            'data' => null
        ];
        
    } catch (Exception $e) {
        $this->db->rollBack();
        
        return [
            'code' => 400,
            'msg' => '批量撤回失败: ' . $e->getMessage(),
            'data' => null
        ];
    }
}
```

### Q5: 如何实现撤回操作的审计日志？

在基类中添加审计日志方法：

```php
// 在 CountryBase 类中添加
protected function logAudit(string $action, string $recordId, string $code, array $details = []): void
{
    try {
        $sql = "INSERT INTO audit_logs 
                (action, record_id, code, details, created_at) 
                VALUES (?, ?, ?, ?, GETDATE())";
        
        $stmt = $this->db->prepare($sql);
        $stmt->execute([
            $action,
            $recordId,
            $code,
            json_encode($details)
        ]);
    } catch (PDOException $e) {
        // 审计日志失败不应影响主业务
        error_log("Audit log failed: " . $e->getMessage());
    }
}
```

在应用方法中使用：

```php
public function myapp(string $id, string $code): array
{
    $result = $this->execute($sql, $params);
    
    if ($result['code'] === 200) {
        // 记录审计日志
        $this->logAudit('revoke', $id, $code, [
            'app_name' => 'myapp',
            'timestamp' => date('Y-m-d H:i:s')
        ]);
    }
    
    return $result;
}
```

## 性能优化建议

### 1. 使用索引

确保数据库表的匹配字段有索引：

```sql
-- 为常用查询字段创建索引
CREATE INDEX idx_source_record_id ON uk_vat_register(source_record_id);
CREATE INDEX idx_registration_status ON uk_vat_register(registration_status);
```

### 2. 批量操作优化

对于批量更新，使用 IN 子句：

```php
public function batch_revoke_optimized(array $ids, string $code): array
{
    if (empty($ids)) {
        return [
            'code' => 400,
            'msg' => 'ID 列表不能为空',
            'data' => null
        ];
    }
    
    // 构造 IN 子句的占位符
    $placeholders = str_repeat('?,', count($ids) - 1) . '?';
    
    $sql = "UPDATE my_table 
            SET status = ? 
            WHERE record_id IN ($placeholders)";
    
    $params = array_merge(['revoked'], $ids);
    
    $result = $this->execute($sql, $params);
    
    return [
        'code' => 200,
        'msg' => '批量撤回成功',
        'data' => null
    ];
}
```

### 3. 连接池配置

在 `config/database.php` 中启用持久连接：

```php
return [
    'host' => 'localhost',
    'database' => 'mydb',
    'username' => 'user',
    'password' => 'pass',
    'options' => [
        PDO::ATTR_PERSISTENT => true,  // 启用持久连接
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    ]
];
```

### 4. 缓存优化

对于频繁查询的配置数据，使用缓存：

```php
private static $configCache = [];

protected function getConfig(string $key)
{
    if (!isset(self::$configCache[$key])) {
        // 从数据库加载配置
        self::$configCache[$key] = $this->loadConfigFromDb($key);
    }
    
    return self::$configCache[$key];
}
```


## 安全最佳实践

### 1. 防止 SQL 注入

**始终使用参数化查询**，永远不要直接拼接 SQL：

```php
// ✅ 安全：参数化查询
$sql = "UPDATE users SET status = ? WHERE id = ?";
$params = [$status, $id];

// ❌ 危险：SQL 注入风险
$sql = "UPDATE users SET status = '$status' WHERE id = '$id'";
```

### 2. 输入验证和清理

验证所有输入参数：

```php
private function validateId(string $id): bool
{
    // 只允许字母、数字、连字符和下划线
    return preg_match('/^[A-Za-z0-9\-_]+$/', $id) === 1;
}

private function validateCode(string $code): bool
{
    // 只允许字母和数字
    return preg_match('/^[A-Za-z0-9]+$/', $code) === 1;
}
```

### 3. 错误信息安全

不要在错误消息中暴露敏感信息：

```php
// ✅ 安全：通用错误消息
return [
    'code' => 400,
    'msg' => '操作失败，请稍后重试',
    'data' => null
];

// ❌ 危险：暴露数据库结构
return [
    'code' => 400,
    'msg' => 'Table uk_vat_register column registration_status not found',
    'data' => null
];
```

### 4. 访问控制（可选）

在 `index.php` 中添加 API 密钥验证：

```php
// 验证 API 密钥
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';

if ($apiKey !== 'your-secret-api-key') {
    Response::json(401, '未授权访问', null);
}
```

### 5. 速率限制（可选）

防止滥用，实现简单的速率限制：

```php
// 简单的速率限制示例
$clientIp = $_SERVER['REMOTE_ADDR'];
$cacheKey = "rate_limit_$clientIp";

// 使用 APCu 或 Redis 存储请求计数
if (apcu_exists($cacheKey)) {
    $count = apcu_fetch($cacheKey);
    if ($count > 100) {  // 每分钟最多 100 次请求
        Response::json(429, '请求过于频繁，请稍后重试', null);
    }
    apcu_inc($cacheKey);
} else {
    apcu_store($cacheKey, 1, 60);  // 60 秒过期
}
```

## 部署清单

在将代码部署到生产环境之前，请确认以下事项：

### 环境配置

- [ ] PHP 版本 >= 8.3
- [ ] 已安装 PDO 扩展
- [ ] 已安装 pdo_sqlsrv 扩展（SQL Server 驱动）
- [ ] Web 服务器配置正确（Apache/Nginx）
- [ ] 文件权限设置正确

### 数据库配置

- [ ] 数据库连接信息已正确配置
- [ ] 数据库用户权限已正确设置
- [ ] 相关表和字段已创建
- [ ] 索引已创建以优化性能

### 安全配置

- [ ] 生产环境已关闭错误显示（`display_errors = 0`）
- [ ] 错误日志已启用（`log_errors = 1`）
- [ ] 敏感信息（数据库密码）未硬编码
- [ ] 已实现访问控制（如需要）
- [ ] 已配置 HTTPS（推荐）

### 代码检查

- [ ] 所有国家类已测试
- [ ] 所有应用方法已测试
- [ ] 错误处理已完善
- [ ] 日志记录已配置
- [ ] 代码已通过审查

### 文档

- [ ] API 使用文档已更新
- [ ] 开发者指南已更新
- [ ] 支持的国家和应用列表已更新
- [ ] 变更日志已记录

## 版本控制建议

### Git 工作流

```bash
# 创建功能分支
git checkout -b feature/add-us-country

# 添加新文件
git add countries/us.php

# 提交更改
git commit -m "feat: 添加美国（US）国家类和 taxid 应用方法"

# 推送到远程仓库
git push origin feature/add-us-country

# 创建 Pull Request 进行代码审查
```

### 提交消息规范

使用语义化提交消息：

- `feat:` 新功能（例如：添加新国家类）
- `fix:` 修复 bug
- `docs:` 文档更新
- `refactor:` 代码重构
- `test:` 添加测试
- `chore:` 构建过程或辅助工具的变动

示例：

```
feat: 添加德国（DE）国家类
fix: 修复 gb::vatreg 方法的参数验证问题
docs: 更新开发者指南中的数据库操作示例
refactor: 优化 CountryBase::execute 方法的错误处理
```

## 获取帮助

### 资源链接

- **PHP 官方文档**: https://www.php.net/manual/zh/
- **PDO 文档**: https://www.php.net/manual/zh/book.pdo.php
- **SQL Server PHP 驱动**: https://docs.microsoft.com/en-us/sql/connect/php/

### 联系方式

如有问题或需要支持，请联系：

- 技术支持邮箱: [support@example.com]
- 开发团队: [dev-team@example.com]
- 项目仓库: [GitHub/GitLab URL]

## 附录

### A. 快速参考

#### 创建新国家类的步骤

1. 在 `countries/` 目录创建 `{国家代码}.php` 文件
2. 定义类并继承 `CountryBase`
3. 实现应用方法（签名: `public function {app_name}(array $requiredParams): array`）
4. 使用 `$this->execute($sql, $params)` 执行数据库操作
5. 返回标准格式的结果数组
6. 测试并部署

#### 标准返回格式

```php
// 成功
['code' => 200, 'msg' => '操作成功', 'data' => null]

// 失败
['code' => 400, 'msg' => '错误描述', 'data' => null]
```

#### 常用 SQL 模式

```php
// 更新状态
$sql = "UPDATE {table} SET {status_field} = ? WHERE {id_field} = ?";
$params = [{status_value}, $id];

// 软删除
$sql = "UPDATE {table} SET deleted_at = GETDATE(), is_deleted = 1 WHERE {id_field} = ?";
$params = [$id];

// 带时间戳的更新
$sql = "UPDATE {table} SET {status_field} = ?, updated_at = GETDATE() WHERE {id_field} = ?";
$params = [{status_value}, $id];
```

### B. 代码模板文件

将以下模板保存为 `templates/country_template.php` 供参考：

```php
<?php

/**
 * {国家代码} - {国家名称}国家处理类
 * 
 * 处理{国家名称}相关的应用撤回请求
 */
class {国家代码} extends CountryBase
{
    /**
     * 处理{应用描述}撤回请求
     * 
     * @param string $id 记录的唯一标识符
     * @param string $code 流水号
     * @return array 返回标准格式的结果数组（code, msg, data）
     */
    public function {应用名称}(array $requiredParams): array
    {
        $businessSerialNumber = $requiredParams['BusinessSerialNumber'];

        // 1. 先查询当前状态，仅 status=0 或 status=-1 才允许撤回
        $checkSql = "SELECT status FROM {表名} WHERE {匹配字段} = ?";
        $checkResult = $this->fetchOne($checkSql, [$businessSerialNumber]);

        if ($checkResult['code'] !== 200) {
            return [
                'code' => 400,
                'msg' => '未找到指定的记录',
                'data' => null
            ];
        }

        $currentStatus = $checkResult['data']['status'];
        if ($currentStatus != 0 && $currentStatus != -1) {
            return [
                'code' => 400,
                'msg' => '该记录当前状态不允许撤回',
                'data' => null
            ];
        }

        // 2. 构造 SQL 语句
        $sql = "UPDATE {表名} 
                SET {状态字段} = ? 
                WHERE {匹配字段} = ?";
        
        $params = [5, $businessSerialNumber];
        
        // 3. 执行数据库操作
        $result = $this->execute($sql, $params);
        
        // 4. 处理结果
        if ($result['code'] === 200) {
            $affectedRows = $result['data']['affected_rows'] ?? 0;
            
            if ($affectedRows > 0) {
                return [
                    'code' => 200,
                    'msg' => '{应用描述}撤回成功',
                    'data' => null
                ];
            } else {
                return [
                    'code' => 400,
                    'msg' => '未找到指定的{应用描述}记录',
                    'data' => null
                ];
            }
        }
        
        return [
            'code' => 400,
            'msg' => '{应用描述}撤回失败: ' . $result['msg'],
            'data' => null
        ];
    }
}
```

---

**文档版本**: 1.1  
**最后更新**: 2026-07-06  
**维护者**: 开发团队

