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)vsbin2hex(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
相关产品推荐
相关产品推荐

