本文目录导读:

在PHP项目中,基于IP进行地域推送(即地理定位)并分组用户,通常需要以下几个核心步骤,这里提供一个从基础到深入的完整方案。
核心流程
- 获取用户IP:从
$_SERVER中获取。 - IP转地域:使用IP定位库或API将IP转换为地理位置(国家、省份、城市)。
- 存储/匹配:将地域信息与用户会话或用户数据进行关联,用于后续推送。
- 执行推送:根据用户的地域分组,推送差异化的内容。
第一步:获取用户真实IP
这是最关键的一步,因为用户可能使用代理、CDN或负载均衡。
<?php
function getRealIpAddr() {
// 优先检查HTTP头(CDN、反向代理等)
$headers = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'REMOTE_ADDR'];
foreach ($headers as $key) {
if (!empty($_SERVER[$key])) {
$ipList = explode(',', $_SERVER[$key]); // X-Forwarded-For 可能包含逗号分隔的多个IP
$ip = trim($ipList[0]); // 取第一个(最接近客户端的)
// 验证IP格式,防止伪造
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
}
}
return '0.0.0.0'; // 默认值
}
$userIp = getRealIpAddr();
echo $userIp; // 输出:192.168.1.100 或 真实公网IP
关键点:
- 不要直接使用
$_SERVER['REMOTE_ADDR'],因为它会被代理掩盖。 X-Forwarded-For可能有多个IP,取第一个是客户端真实IP,后续的是代理服务器IP。- 需要配置可信代理(如Nginx、Cloudflare),防止用户伪造IP。
第二步:IP转地域(核心)
有几种常见方案,按推荐度排序:
方案A:使用离线IP数据库(推荐,高性能,无外部依赖)
最成熟的选择是 IPIP.net 或 纯真IP库,这里以 IPIP.net 的免费库为例:
-
下载数据库文件:从 IPIP.net 下载
ipipfree.ipdb。 -
使用Composer安装:
composer require ipip/ipip-php
-
示例代码:
<?php require_once 'vendor/autoload.php'; use ipip\db\City; $city = new City(__DIR__ . '/ipipfree.ipdb'); // 路径要正确 function getLocationByIp($ip) { global $city; try { // 返回数组:['中国', '北京', '北京', '', '100000'] $arr = $city->find($ip, 'CN'); // 'CN' 返回中文 return [ 'country' => $arr[0] ?? '', 'province' => $arr[1] ?? '', 'city' => $arr[2] ?? '', 'isp' => $arr[4] ?? '' ]; } catch (\Exception $e) { return null; // 未找到或IP无效 } } $location = getLocationByIp('8.8.8.8'); print_r($location); // ['country'=>'美国', 'province'=>'', 'city'=>'', 'isp'=>'']
优点:离线、无延迟、不依赖第三方服务、准确率高(付费版更高)。 缺点:免费库数据更新较慢(约1次/月),付费版支持省份和城市。
方案B:使用在线API(简单,但依赖外部服务)
ip-api.com 或 淘宝IP库。
<?php
function getLocationByApi($ip) {
$url = "http://ip-api.com/json/{$ip}?lang=zh-CN";
$response = file_get_contents($url);
$data = json_decode($response, true);
if ($data && $data['status'] === 'success') {
return [
'country' => $data['country'] ?? '',
'province' => $data['regionName'] ?? '',
'city' => $data['city'] ?? '',
'isp' => $data['isp'] ?? ''
];
}
return null;
}
优点:实现简单,无需维护数据库。 缺点:
- 有请求频率限制(ip-api.com 匿名用户每分钟45次,付费无限制)。
- 依赖网络,较慢。
- 有隐私风险(用户IP发到第三方)。
方案C:使用Cloudflare或CDN提供的头部
如果你使用了Cloudflare等CDN,它们会在HTTP头中注入地域信息。
Nginx配置示例:
# 让Nginx从X-Forwarded-For获取并传递给PHP set_real_ip_from 0.0.0.0/0; real_ip_header X-Forwarded-For;
然后在PHP中:
$countryCode = $_SERVER['HTTP_CF_IPCOUNTRY'] ?? ''; // Cloudflare专用 $region = $_SERVER['HTTP_X_AMZ_CF_ID'] ?? ''; // AWS CloudFront 的示例
优点:零延迟,最准确(因为来自网络边缘)。 缺点:仅限特定CDN用户。
第三步:用户分组与存储
获取到地域信息后,你需要将用户分组,常见的做法:
基于Session(临时分组,适合未登录用户)
session_start();
$_SESSION['user_location'] = [
'country' => $location['country'],
'province' => $location['province'],
'city' => $location['city'],
'group' => $location['province'] ?: $location['country'] // 分组键
];
基于数据库(持久化,适合登录用户)
假设你有 users 表,增加字段:
ALTER TABLE users ADD COLUMN country varchar(100) DEFAULT NULL; ALTER TABLE users ADD COLUMN province varchar(100) DEFAULT NULL; ALTER TABLE users ADD COLUMN city varchar(100) DEFAULT NULL; ALTER TABLE users ADD COLUMN location_group varchar(50) DEFAULT NULL; -- 分组索引
登录或每次请求时更新:
// 用户登录后,或者每天第一次请求时
if (empty($user['location_group'])) {
$location = getLocationByIp($userIp);
if ($location) {
$group = determineGroup($location); // 自定义分组逻辑
DB::update('users', [
'country' => $location['country'],
'province' => $location['province'],
'city' => $location['city'],
'location_group' => $group
], ['id' => $userId]);
}
}
分组逻辑示例:
function determineGroup($location) {
// 按省份分组(最常用)
if (!empty($location['province'])) {
return 'region_' . $location['province']; // 如 region_北京
}
// 按国家分组(国外用户)
if (!empty($location['country'])) {
return 'country_' . $location['country']; // 如 country_美国
}
return 'unknown';
}
基于Redis/ZSet(实时统计分组)
对于实时推送,可以将用户地域信息存入Redis集合:
SADD users:region:北京 user123 SADD users:region:上海 user456
第四步:执行地域推送
场景A:网页内内容差异化(最常见)
if ($user['location_group'] === 'region_广东') {
echo '<div class="local-news">广东本地新闻推送...</div>';
} elseif ($user['location_group'] === 'region_北京') {
echo '<div class="local-news">北京本地活动推送...</div>';
} else {
echo '<div class="global-news">通用内容</div>';
}
场景B:后端API推送(如通过队列发送邮件/通知)
// 获取所有广东用户
$users = DB::select("SELECT email FROM users WHERE location_group = 'region_广东'");
foreach ($users as $user) {
// 发送邮件:广东特有的优惠券
sendEmail($user->email, '广东专属活动', '...');
}
场景C:实时WebSocket推送(如弹窗、站内信)
使用Redis Pub/Sub + WebSocket服务(如Workerman、Swoole):
// 某用户访问时,将其加入对应的地域频道
$redis->subscribe('channel:region_上海', function($message) use ($userWsConnection) {
$userWsConnection->send($message);
});
// 后台触发推送:给上海用户发送消息
$redis->publish('channel:region_上海', json_encode(['type'=>'push', 'content'=>'...']));
性能与优化建议
- 缓存地域信息:不要每次请求都查IP库,推荐:
- 使用Redis缓存
ip -> location映射,过期时间1天。 - 或者将用户地域信息存入Session/User表,只首次查询。
- 使用Redis缓存
- IP库放内存:离线IP库(如IPIP)加载后可以放入
APCu或共享内存,避免重复文件读取。 - 异步更新:对于非关键页面(如首页),可以用AJAX异步获取地域并更新session,不影响首屏速度。
- 检测代理:如果使用付费CDN,确保Nginx/PHP正确设置了
set_real_ip_from信任代理IP段。
完整示例流程
<?php
// 1. 获取IP
$ip = getRealIpAddr();
// 2. 尝试从Redis缓存获取
$location = $redis->get("location:$ip");
if (!$location) {
// 2.1 查询IP库
$location = getLocationByIp($ip);
// 2.2 缓存1天
$redis->setex("location:$ip", 86400, json_encode($location));
} else {
$location = json_decode($location, true);
}
// 3. 分组(假设用户已登录,ID=123)
$group = determineGroup($location);
if ($group !== $userFromDB['location_group']) {
// 更新数据库,用于后续推送
DB::update('users', ['location_group' => $group], ['id' => 123]);
}
// 4. 页面根据分组显示不同内容
if ($group === 'region_北京') {
showBeijingContent();
} else {
showDefaultContent();
}
- 推荐:使用离线IP库(IPIP/纯真)+ Redis缓存 + 持久化用户分组。
- 关键点:获取真实IP、选择准确的IP转地域方案、合理设计分组键(建议用省份/城市粒度)。
- 错误处理:IP库查不到或APi返回失败时,使用默认分组(如
unknown),不要白屏。