Appearance
客服系统怎么按访客 IP 属地分配坐席?PHP 实现方言路由
一个做本地生活平台的读者问过我一个问题:他们平台在 6 个省份运营,客服坐席集中在总部,结果广东客户一句粤语开场,普通话客服愣了三秒,然后开始「您请说普通话」——客户扭头就走。他想要的是:访客进线时,系统自动看他从哪来,把会话路由给能说当地方言的坐席,或者本地城市的坐席。这个需求用 IP 归属地就能实现,而且不依赖定位权限、不需要访客填任何表单。本文用 PHP 写一个完整的分配示例。
一、为什么要按 IP 属地分配坐席
「在线客服显示访客 IP 属地」只是展示层,分配是更进一步的动作——把属地从「看」变成「用」:
- 方言匹配:广东、四川、东北的客户,本地坐席接单转化率肉眼可见地高;
- 本地业务:医美、房产、教培这类强地域属性行业,客户只信本地门店,坐席得会说本地政策、本地价格;
- 时区与话术:跨时区业务(比如海外华人市场)按地区分配坐席,避免凌晨三点把客户转给刚醒来的值班员;
- 合规属地:部分行业要求咨询必须由具备当地资质的坐席承接,属地路由是硬需求。
难点在于怎么低成本拿到访客城市。前端定位要授权弹窗、会被用户拒绝;服务端查 IP 归属地零感知、零权限,是这类场景的第一选择。
二、方案设计
访客发起会话,PHP 后端做四步:
- 取真实客户端 IP——Nginx/CDN 后面要小心,
X-Forwarded-For可以被伪造,取最右一个非可信代理的 IP; - 调 IP9 免费接口
https://ip9.com.cn/get?ip=<ip>,拿到省份、城市、运营商(prov/city/isp字段); - 拿城市去匹配坐席表:坐席记录里带
prov、city、group_id字段,命中同城/同省坐席组就分配过去,没命中落默认组; - 把地区标签写进会话上下文,客服工作台直接展示「客户来自广东-广州」,话术上来就能切。
接口调用注意两点:免费版 60 次/分钟,必须加缓存(同一个 IP 一天内查一次就够);接口偶发失败时降级到默认坐席组,别让客服系统整个挂掉。
三、PHP 实现
下面是一个完整可运行的分配脚本:查归属地(带文件缓存)→ 按省市匹配坐席组 → 返回分配结果。跑起来只需要一个 PHP 环境和一张坐席表。
php
<?php
// 客服按 IP 属地分配坐席:assign_by_geo.php
// 依赖:PHP 7.4+,启用 curl 扩展
const GEO_API = 'https://ip9.com.cn/get?ip=%s';
const CACHE_TTL = 86400; // 归属地缓存 24 小时
/** 取访客真实 IP:直连取 REMOTE_ADDR,反代场景取 XFF 最右一个可信值 */
function client_ip(): string {
$xff = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';
if ($xff !== '') {
// XFF 可伪造,这里用「从右往左第一个非内网」的简化策略
$parts = array_reverse(array_map('trim', explode(',', $xff)));
foreach ($parts as $p) {
if (filter_var($p, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $p;
}
}
}
return $_SERVER['REMOTE_ADDR'] ?? '';
}
/** 查 IP 归属地,带文件缓存;失败返回 null(降级用) */
function geo_lookup(string $ip): ?array {
$cacheFile = sys_get_temp_dir() . '/geo_' . md5($ip) . '.json';
if (is_file($cacheFile) && filemtime($cacheFile) > time() - CACHE_TTL) {
$cached = json_decode(file_get_contents($cacheFile), true);
if (is_array($cached)) return $cached;
}
$url = sprintf(GEO_API, $ip);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_USERAGENT => 'support-router/1.0',
]);
$body = curl_exec($ch);
curl_close($ch);
$data = json_decode((string)$body, true);
if (!is_array($data) || ($data['ret'] ?? 0) !== 200) {
return null;
}
$info = ['prov' => $data['data']['prov'] ?? '', 'city' => $data['data']['city'] ?? ''];
file_put_contents($cacheFile, json_encode($info)); // 同一 IP 只查一次
return $info;
}
/** 坐席表:字段 group_id / name / prov / city(空 city = 覆盖全省) */
function load_agents(): array {
return [
['group_id' => 1, 'name' => '广佛组', 'prov' => '广东', 'city' => '广州'],
['group_id' => 1, 'name' => '广佛组', 'prov' => '广东', 'city' => '佛山'],
['group_id' => 2, 'name' => '川渝组', 'prov' => '四川', 'city' => ''],
['group_id' => 2, 'name' => '川渝组', 'prov' => '重庆', 'city' => ''],
['group_id' => 3, 'name' => '东北组', 'prov' => '辽宁', 'city' => ''],
['group_id' => 9, 'name' => '默认组', 'prov' => '', 'city' => ''],
];
}
/** 按省市匹配坐席组:先精确到城市,再放宽到省,最后默认组 */
function assign_group(string $prov, string $city): int {
$default = 9;
foreach (load_agents() as $a) {
if ($a['prov'] === $prov && $a['city'] === $city) return $a['group_id'];
}
foreach (load_agents() as $a) {
if ($a['prov'] === $prov && $a['city'] === '') return $a['group_id'];
}
return $default;
}
// --- 入口 ---
$ip = client_ip();
$geo = geo_lookup($ip);
if ($geo === null) {
// 接口故障降级:默认组,不阻塞会话
$group = assign_group('', '');
$region = 'unknown';
} else {
$group = assign_group($geo['prov'], $geo['city']);
$region = $geo['prov'] . '-' . $geo['city'];
}
echo json_encode([
'visitor_ip' => $ip,
'region' => $region,
'group_id' => $group,
'agents' => array_values(array_filter(load_agents(), fn($a) => $a['group_id'] === $group)),
], JSON_UNESCAPED_UNICODE);在真实项目里,把 load_agents() 换成查数据库或接口、assign_group 的结果写进会话表即可。客服工作台拿到 region 字段,进线第一时间就能看到「广东-广州」,方言打招呼不再是碰运气。
四、落地注意事项
- 代理和加速器会带偏属地。VPN、游戏加速器会把访客 IP 显示到别的城市,导致错配。两类兜底:允许客户在会话里手动选择城市并记住;或者干脆把属地路由当「第一选择」,人工可改。
- 机房流量要识别。很多咨询机器人跑在机房 IP 上,把它们路由给真坐席是浪费人力。IP9 免费版能看运营商(
isp字段),VIP 版还能返回ip_type(ISP 家庭 / BUS 企业 / IDC 机房)和区县area,把 IDC 来源的会话标记出来单独处理,效率高很多。 - 别把缓存做成定时炸弹。文件缓存在单机够用,多实例部署就换 Redis;TTL 别设太短,否则免费额度很容易被高峰流量吃满。
- 隐私合规:记录访客 IP 用于路由,属于正当业务目的,但要在隐私政策里写明用途和保留期限。
总结
按 IP 属地分配客服坐席,本质是给「进线」这个动作提前加上地域上下文。PHP 实现不超过 80 行:取 IP → 查 IP9 免费接口 https://ip9.com.cn/get?ip=xxx(无注册、60 次/分钟,缓存后量级可忽略)→ 省市匹配坐席组 → 降级兜底。先让客户听到熟悉的方言,再谈转化率。官网:https://www.ip9.com.cn 。