# 开发者指南

## 概述

本文档为开发者提供详细的指导，说明如何扩展应用撤回接口系统。通过本指南，您将学会如何添加新的国家类、实现新的应用方法，以及遵循系统的最佳实践和规范。

## 系统架构概览

系统采用面向对象的设计模式，核心组件包括：

- **API 入口** (`index.php`): 接收请求并路由到相应的处理类
- **数据库管理** (`core/Database.php`): 单例模式管理数据库连接
- **响应处理** (`core/Response.php`): 统一的 JSON 响应格式
- **国家基类** (`core/CountryBase.php`): 提供数据库连接和通用方法
- **国家类** (`countries/*.php`): 实现特定国家的应用处理逻辑

### 目录结构

```
project/
├── index.php                 # 撤回 API 入口文件
├── api.php                   # 文件生成转发 API 入口文件
├── config/
│   ├── database.php         # 数据库配置
│   └── downstream.php       # 转发下游 *_API_URL 常量（部署时填写）
├── 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  # 开发者指南
    └── UNIFIED_API_DESIGN.md  # 统一中转 API 规范
```

## 添加新国家类

### 步骤 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();
        // 添加特定的初始化逻辑
    }
    
    // 在这里添加应用方法...
}
```

**完整示例**: 法国（FR）国家类

```php
<?php

/**
 * fr - 法国国家处理类
 * 
 * 处理法国相关的应用撤回请求
 */
class fr extends CountryBase
{
    /**
     * 处理法国增值税注册（VAT Registration）撤回请求
     * 更新 vat_fr_register 表中的 status 为 5（已撤回状态）
     * @param array $requiredParams
     * @return array 返回标准格式的结果数组（code, msg, data）
     */
    public function vatpushtax_101($requiredParams): array
    {
        $businessSerialNumber = $requiredParams['BusinessSerialNumber'];

        // 先查询当前状态，只有 status=0（待处理）或 status=1（处理中）才允许撤回
        $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 语句  将 status 更新为 5（撤回状态）
        $sql = "UPDATE vat_fr_register 
                SET status = ? 
                WHERE tid = ?";
        
        // 参数数组：[新状态值, 记录ID]
        $params = [5, $businessSerialNumber];
        
        // 执行 SQL 更新操作
        $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
                ];
            }
        } else {
            // 数据库操作失败
            return [
                'code' => 400,
                'msg' => '法国VAT注册撤回失败: ' . $result['msg'],
                'data' => null
            ];
        }
    }
        /**
     * 处理法国增值税注册（VAT Registration）撤回请求
     * 更新 vat_fr_register 表中的 status 为 5（已撤回状态）
     * @param array $requiredParams
     * @return array 返回标准格式的结果数组（code, msg, data）
     */
    public function vatpushtax_105($requiredParams): array
    {
        $businessSerialNumber = $requiredParams['BusinessSerialNumber'];

        // 先查询当前状态，只有 status=0（待处理）或 status=1（处理中）才允许撤回
        $checkSql = "SELECT step1_status FROM eori_fr_register WHERE tid = ?";
        $checkResult = $this->fetchOne($checkSql, [$businessSerialNumber]);

        if ($checkResult['code'] !== 200) {
            return [
                'code' => 400,
                'msg' => '未找到指定的法国VAT注册记录',
                'data' => null
            ];
        }

        $currentStatus = $checkResult['data']['step1_status'];
        if ($currentStatus != 0 && $currentStatus != -1) {
            return [
                'code' => 400,
                'msg' => '该记录当前状态不允许撤回',
                'data' => null
            ];
        }

        // 构造 SQL UPDATE 语句  将 status 更新为 5（撤回状态）
        $sql = "UPDATE eori_fr_register 
                SET step1_status = ? 
                WHERE tid = ?";
        
        // 参数数组：[新状态值, 记录ID]
        $params = [5, $businessSerialNumber];
        
        // 执行 SQL 更新操作
        $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
                ];
            }
        } else {
            // 数据库操作失败
            return [
                'code' => 400,
                'msg' => '法国VAT注册撤回失败: ' . $result['msg'],
                '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}(string $id, string $code): 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 {应用名称}(string $id, string $code): array
    {
        // 1. 参数验证（可选）
        if (empty($id)) {
            return [
                'code' => 400,
                'msg' => '参数验证失败: ID 不能为空',
                'data' => null
            ];
        }
        
        // 2. 构造 SQL 语句
        $sql = "UPDATE {表名} 
                SET {状态字段} = ? 
                WHERE {匹配字段} = ?";
        
        $params = [{撤回状态值}, $id];
        
        // 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.0  
**最后更新**: 2024  
**维护者**: 开发团队

