畅捷通Helper 工具库:通用函数设计与最佳实践

一、sync_helper.php 总览

sync_helper.php 是整个项目的基础设施层,提供了 Token 生命周期管理、API 调用封装、数据库连接、日志记录等通用功能。所有 6 个同步模块都依赖它,无需重复实现。

二、配置管理

2.1 config.json 结构

复制代码
{
    "appKey": "IERNZ8N49YRWRM6OBPCDQKE0WY9AK7UZ",
    "appSecret": "YOUR_APP_SECRET",
    "certificate": "YOUR_CERTIFICATE",
    "secretKey": "YOUR_SECRET_KEY",
    "openToken": "eyJhbG...",
    "refreshToken": "def502...",
    "token_expiry": 1718640000,
    "refresh_expiry": 1739404800,
    "db": {
        "host": "127.0.0.1",
        "port": "3306",
        "name": "sq_t1",
        "user": "root",
        "pass": "your_password"
    }
}
​

2.2 动态更新配置

复制代码
function loadConfig() {
    $json = file_get_contents(__DIR__ . '/config.json');
    return json_decode($json, true);
}

function saveConfig($config) {
    file_put_contents(
        __DIR__ . '/config.json',
        json_encode($config, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
    );
}
​

💡 设计要点getValidToken() 使用引用传递 &$config ,在刷新 Token 后直接更新配置数组,再调用 saveConfig() 持久化。外部调用方无需关心 Token 更新细节。

三、Token 生命周期管理

3.1 智能刷新策略

3.2 关键实现

复制代码
function getValidToken(&$config) {
    $now = time();
    $tokenExpiry = $config['token_expiry'] ?? 0;

    // 提前 10 分钟刷新,避免边缘情况
    if (!empty($config['openToken']) && $now < ($tokenExpiry - 600)) {
        return $config['openToken'];
    }

    // 优先使用 refreshToken(减少 API 调用)
    if (!empty($config['refreshToken']) && $now < ($config['refresh_expiry'] ?? 0)) {
        try {
            $newToken = refreshToken($config);
            if ($newToken) return $newToken;
        } catch (Exception $e) {
            logMsg("refreshToken 失败,走完整流程: " . $e->getMessage());
        }
    }

    // 完整流程
    return fullTokenFlow($config);
}
​

四、AES 解密实现

4.1 算法参数

参数
算法 AES-128-ECB
密钥长度 128 bit (16 byte)
填充方式 PKCS5 Padding
输入格式 Base64 编码密文
输出格式 JSON(含 bizContent.appTicket)

4.2 完整解密函数

复制代码
function decryptAppTicket($encryptMsg, $secretKey) {
    // 1. Base64 解码
    $decoded = base64_decode($encryptMsg);
  
    // 2. AES-128-ECB 解密
    $decrypted = openssl_decrypt(
        $decoded,
        'AES-128-ECB',
        $secretKey,
        OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING
    );

    if ($decrypted === false) {
        // 降级尝试:使用 PKCS7 填充模式
        $decrypted = openssl_decrypt(
            $decoded,
            'AES-128-ECB',
            $secretKey,
            OPENSSL_RAW_DATA
        );
    }

    if ($decrypted === false) {
        throw new Exception("AES 解密失败");
    }

    // 3. 手动去除 PKCS5 尾部填充
    $pad = ord($decrypted[strlen($decrypted) - 1]);
    if ($pad > 0 && $pad <= 16) {
        $decrypted = substr($decrypted, 0, -$pad);
    }

    // 4. 解析 JSON 提取 appTicket
    $data = json_decode($decrypted, true);
    if (!isset($data['bizContent']['appTicket'])) {
        throw new Exception("appTicket 缺失");
    }

    return $data['bizContent']['appTicket'];
}
​

4.3 多密钥兼容机制

五、API 调用封装

5.1 通用 HTTP 调用

复制代码
function callApi($url, $data, $headers = []) {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => is_array($data) ? json_encode($data) : $data,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 60,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_SSL_VERIFYPEER => false,  // 内网环境
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error    = curl_error($ch);
    curl_close($ch);

    if ($error) {
        throw new Exception("cURL 错误: {$error}");
    }

    $data = json_decode($response, true);
    if ($httpCode >= 400) {
        throw new Exception("HTTP {$httpCode}: " . ($data['message'] ?? $response));
    }

    return $data;
}
​

5.2 T+ API 专用封装

复制代码
function callTplusApi($appKey, $appSecret, $openToken, $url, $body) {
    $headers = [
        'Content-Type: application/json',
        "appKey: {$appKey}",
        "appSecret: {$appSecret}",
        "openToken: {$openToken}",
    ];

    return callApi($url, $body, $headers);
}
​

💡 自动在 Header 中注入 appKeyappSecretopenToken 三个认证头,调用方无需手动拼接。

六、数据处理工具

6.1 extractRows - 兼容多种响应格式

不同 API 返回格式各异,extractRows() 统一处理:

复制代码
function extractRows($res) {
    // 格式1: Data 键直接包含数组(Query 接口)
    if (isset($res['Data']) && is_array($res['Data'])) {
        return $res['Data'];
    }
    // 格式2: result.Data(某些接口有 result 包裹)
    if (isset($res['result']['Data']) && is_array($res['result']['Data'])) {
        return $res['result']['Data'];
    }
    // 格式3: 直接是数组列表
    if (is_array($res) && isset($res[0])) {
        return $res;
    }
    return [];
}
​

6.2 toBool - 布尔值标准化

畅捷通 API 返回的布尔值可能是 true/false(JSON)、"true"/"false"(字符串)、1/0(整数):

复制代码
function toBool($val) {
    if (is_bool($val)) return $val;
    if (is_string($val)) return strtolower($val) === 'true' || $val === '1';
    if (is_numeric($val)) return intval($val) === 1;
    return (bool) $val;
}
​

七、数据库连接

7.1 PDO 单例

复制代码
function getDB($dbConfig) {
    static $pdo = null;
    if ($pdo === null) {
        $dsn = "mysql:host={$dbConfig['host']};port={$dbConfig['port']};dbname={$dbConfig['name']};charset=utf8mb4";
        $pdo = new PDO($dsn, $dbConfig['user'], $dbConfig['pass'], [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,
        ]);
    }
    return $pdo;
}
​

💡 static $pdo = null 确保同一请求内只创建一次数据库连接,避免重复连接开销。

八、日志系统

复制代码
function logMsg($msg) {
    $time = date('Y-m-d H:i:s');
    echo "[{$time}] {$msg}\n";
    // 也可扩展写入文件
}
​

输出格式:

复制代码
[2026-06-17 08:09:45] 开始同步存货(QueryPage 分页)...
[2026-06-17 08:10:32] 存货 QueryPage 第1页: 写入 200 条, 累计 200 / 7988
​

九、设计原则总结

原则 实现方式
单一职责 Token管理/API调用/数据处理各司其职
DRY 6 个同步模块复用同一套 helper
容错性 多密钥兼容、双重解密模式、降级重试
可观测性 每步操作均有日志输出
幂等性 INSERT ON DUPLICATE KEY UPDATE 保证重复执行安全
安全性 敏感信息存 config.json,不硬编码