You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PHP REST API用户同步跨服务器表现异常的技术问询

问题描述

我们基于PHP开发了一套REST API同步系统,核心功能是对比本地数据库与GpsGate应用的用户表:

  • 若GpsGate中不存在对应用户,则创建该用户;
  • 若用户信息(如邮箱、姓名、RFID等)发生变更,则执行更新操作。

该脚本在测试服务器上运行正常,可按需创建或更新用户。但迁移至另一服务器(数据库结构、表、root账号密码均与测试环境一致)后,出现严重问题:无法识别GpsGate中已存在的用户,误判需创建数万条重复用户数据,执行时因“用户名已存在”报错。


核心PHP代码
require_once('rest_api_includes/Config.php');
require_once(Config::API_INCLUDES_DIR . '/DB.php');
require_once(Config::API_INCLUDES_DIR . '/Gpsgate_API.php');
require_once(Config::API_INCLUDES_DIR . '/Common.php');

class API_Sync
{
    /** @var int application id */
    private $application_id;

    /** @var int user type id */
    private $user_type_id;

    /** @var Gpsgate_API instance */
    private $api;

    /** @var DB instance */
    private $db;

    /** @var string<show|log> Show or log errors */
    private $errors_report_type;

    /** @var string<show|log|none> Show or log actions */
    private $actions_report_type;

    /**
     * Sets variables, connects to database and REST API
     * 
     * @throws \Exception
     */
    public function __construct()
    {
        $this->application_id = Config::API_APPLIATION_ID;
        $this->user_type_id = Config::API_USER_TYPE_ID;
        $this->errors_report_type = Config::API_SHOW_OUTPUT ? 'show' : 'log';
        $this->actions_report_type = Config::API_SHOW_OUTPUT ? 'show' : (Config::API_ACTIONS_LOGGING ? 'log' : 'none');
    }

    /**
     * Main method that controlls all the process
     * 
     * @throws \Exception
     */
    public function run()
    {
        $this->prepare();          

        $gpsgate_users_result = $this->api->getUsers();

        if ($gpsgate_users_result->status !== 200) {
            throw new Exception(implode('; ', $gpsgate_users_result->body));
        }

        $gpsgate_users = $gpsgate_users_result->body;
        $db_users = $this->db->get(DB::TBL_PERSONS);

        $res = $this->compare($gpsgate_users, $db_users);

        $this->updateUsers($res['update']);
        $this->createUsers($res['create']);
    }

    /**
     * Create gsgate users data from database array
     * 
     * @param array $users
     */
    private function createUsers($users)
    {
        if (!empty($users)) {
            $this->logOrShowAction("Begin to create users data.");

            foreach ($users as $user) {
                $res = $this->api->createUser([
                    'email'         => $user['Email'],
                    'name'          => $user['FirstName'],
                    'surname'       => $user['LastName'],
                    'driverID'      => $user['RFID'],
                    'username'      => $user['UserName'],
                    'password'      => Common::generatePassword(),
                    'userTypeId'    => $this->user_type_id,
                    'description'   => $user['Location'],
                ]);

                if ($res->status !== 200) {
                    throw new Exception($res->body);
                }
            }

            $this->logOrShowAction("Done.");
        }
    }

    /**
     * Update gsgate users data from database array
     * 
     * @param array $users
     */
    private function updateUsers($users)
    {
        if (!empty($users)) {
            $this->logOrShowAction("Begin to update users data.");

            foreach ($users as $user) {
                $res = $this->api->updateUser([
                    'email'     => $user['Email'],
                    'name'      => $user['FirstName'],
                    'surname'   => $user['LastName'],
                    'driverID'  => $user['RFID'],
                    'description'  => $user['Location'],
                ], $user['gpsgate_id']);

                if ($res->status !== 200) {
                    throw new Exception($res->body);
                }
            }
            
            $this->logOrShowAction("Done.");
        }
    }

    /**
     * Compare arrays and return list of users data to update/create
     */
    private function compare($gpsgate_users, $db_users)
    {
        $this->logOrShowAction("Begin to compare users data.");

        $gpsgate_user_key = 'username';
        $db_user_key = 'UserName';
        $gpsgate_users = Common::setIndexesByKey($gpsgate_user_key, $gpsgate_users);

        $res = [
            'update' => [],
            'create' => [],
        ];

        foreach ($db_users as $user) {
            $user_key = $user[$db_user_key];

            if (!empty($gpsgate_users[$user_key])) {
                if ($this->userInfoVary($gpsgate_users[$user_key], $user)) {
                    $user['gpsgate_id'] = $gpsgate_users[$user_key]->id; // add gpsgate id
                    $res['update'][] = $user;
                }

            } else {
                $res['create'][] = $user;
            }
        }

        $this->logOrShowAction('Done');
        $this->logOrShowAction('Need to create: ' . count($res['create']) . ' rows; to update: ' . count($res['update']) . ' rows.');

        return $res;
    }

    /**
     * Check is the information between db user and api user is different
     */
    private function userInfoVary($gpsgate_user, $db_user)
    {
        return $db_user['Email'] != $gpsgate_user->email
            || $db_user['FirstName'] != $gpsgate_user->name
            || $db_user['LastName'] != $gpsgate_user->surname
            || $db_user['RFID'] != ($gpsgate_user->driverID ?? '')
            || $db_user['Location'] != $gpsgate_user->description;
    }

    /**
     * Connects to database, REST API, creates folders for errors and actions if need
     */
    private function prepare()
    {
        if ($this->errors_report_type == 'log') {
            if (!file_exists(dirname(Config::API_LOG_FILE_ERRORS))) {
                mkdir(dirname(Config::API_LOG_FILE_ERRORS));
            }
            if (!file_exists(Config::API_LOG_FILE_ERRORS)) {
                file_put_contents(Config::API_LOG_FILE_ERRORS, '');
            }
        }
        if ($this->actions_report_type == 'log') {
            if (!file_exists(dirname(Config::API_LOG_FILE_ACTIONS))) {
                mkdir(dirname(Config::API_LOG_FILE_ACTIONS));
            }
            if (!file_exists(Config::API_LOG_FILE_ACTIONS)) {
                file_put_contents(Config::API_LOG_FILE_ACTIONS, '');
            }
        }

        $this->logOrShowAction('Trying to connect to database and GPSGATE REST API.');

        $this->api = new Gpsgate_API(Config::API_APPLIATION_ID, Config::API_USER_TYPE_ID);
        $this->db = DB::instance();

        $this->logOrShowAction('Done.');
    }

    /**
     * Logs error message in file or output in browser
     * 
     * @param string $msg
     */
    public function logOrShowError($msg)
    {
        $msg = "<span style='color: red; font-weight: 600;'>Error: " . $msg . "</span>";
        $this->writeOrEchoMessage($msg, Config::API_LOG_FILE_ERRORS, $this->errors_report_type);
    }

    /**
     * Logs action message in file or output in browser
     * 
     * @param string $msg
     */
    public function logOrShowAction($msg)
    {
        $this->writeOrEchoMessage($msg, Config::API_LOG_FILE_ACTIONS, $this->actions_report_type);
    }

    private function writeOrEchoMessage($msg, $file, $report_type)
    {
        if ($report_type == 'none') {
            return ;
        }

        $msg = '[' . date('Y-m-d H:i:s') . '] ' . $msg;

        if ($report_type == 'show') {
            echo $msg . '<br>';

        } else {
            $h = fopen($file, 'a+');
            fwrite($h, strip_tags($msg) . PHP_EOL);  
            fclose($h);
        }
    }
}


$sync = new API_Sync();

try {
    $sync->run();

} catch (\Exception $e) {
    $sync->logOrShowError($e->getMessage());
}

排查与解决方案

1. 大小写敏感问题

问题分析

对比用户时,本地数据库用的是UserName字段,GpsGate API返回的是username属性,若新服务器的数据库排序规则为大小写敏感(如utf8_bin),或API返回的用户名大小写与本地库不一致,会导致索引匹配失败,误判用户不存在。

修复方案

统一大小写后再做匹配,修改compare方法中的键处理逻辑:

// 本地数据库用户名转小写
$user_key = strtolower($user[$db_user_key]);
// 同时修改Common::setIndexesByKey方法,将GpsGate的username也转小写作为索引
public static function setIndexesByKey($key, $array) {
    $result = [];
    foreach ($array as $item) {
        if (isset($item->$key)) {
            $result[strtolower($item->$key)] = $item;
        }
    }
    return $result;
}

2. GpsGate API用户列表不完整

问题分析

新服务器调用getUsers()时,可能因API权限不足、分页限制,导致返回的用户列表不全,本地库用户在API结果中找不到,触发创建逻辑。

排查与修复

  • 打印count($gpsgate_users),与GpsGate后台实际用户数对比;
  • 检查Gpsgate_API.php中的getUsers方法,确认是否处理了分页(比如循环调用直到获取全部用户);
  • 验证API账号权限,确保能获取目标应用下的所有用户。

3. 字符编码与不可见字符差异

问题分析

新服务器的PHP环境或数据库字符编码与测试环境不一致,导致用户名存在不可见字符(如首尾空格、全角/半角字符),视觉上相同但实际无法匹配。

排查与修复

  • 对可疑用户名输出十六进制值对比:bin2hex($user_key) vs bin2hex(array_keys($gpsgate_users)[0]);
  • 清洗用户名,去除无效字符:
// 去除首尾空格,转换全角为半角
$user_key = trim(mb_convert_kana($user[$db_user_key], 's'));
  • 统一PHP和数据库的字符编码(推荐utf8mb4)。

4. 索引构建方法异常

问题分析

Common::setIndexesByKey方法未正确将GpsGate用户列表以username为键构建索引,导致$gpsgate_users[$user_key]始终为空。

排查与修复

  • 打印$gpsgate_users的结构,确认索引是否正确;
  • 确保方法正确遍历API返回的对象数组,提取username作为索引(注意API返回的是对象而非数组)。

内容的提问来源于stack exchange,提问作者BradzleyB123

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 14:15:41