Skip to content

网站怎么按访客城市显示本地门店、区域活动?用 IP 归属地自动分发(PHP 实现)

连锁品牌、区域加盟、本地生活类的官网有个共同需求:广州来的访客想看广州门店和广州活动,成都来的想看成都的。给所有人看同一个全国页,等于让一半访客自己再找一遍「离我最近的门店」。让访客手填城市?多一步就多一批流失。服务端用 IP 归属地判断访客所在城市,直接把对应的本地内容给他,访客无感知,这是目前最省事的做法。这篇用 PHP 写一套完整的自动分发:查归属地 → 匹配城市 → 渲染本地门店页,缓存、兜底、SEO 注意事项都带上。

一、为什么是「服务端查 IP」,而不是前端定位

先排除两个想当然的方案:

  • 让浏览器弹定位授权:现在用户对授权弹窗极度警惕,拒绝率很高,而且 HTTPS 下才生效。为了看个门店信息让用户授权位置,转化率直接崩。
  • 纯前端 JS 调查询接口:省了后端,但 IP 归属地接口大多没开浏览器跨域(CORS),直调会被浏览器拦;就算能调,前端每次进站都裸奔一个查询接口,限流和缓存都不好控制。

服务端查 IP 的好处:不用用户授权、不依赖浏览器环境、接口调用次数可控、结果可以缓存,还能顺便给日志打上访客地域标签。IP 归属地是城市级精度,判断「访客在哪个城市」这个需求正好够用——它不是 GPS,别指望精确到街道,那是另一码事。

数据源用 IP9 免费接口 https://ip9.com.cn/get?ip=<ip>,返回国家、省份、城市、运营商等字段,免注册、60 次/分钟/IP。PHP 里一次 file_get_contents 就能拿到 JSON,不需要装任何扩展。

二、方案设计

分发逻辑放在入口文件最前面,三步:

  1. 取访客 IP:直连场景取 REMOTE_ADDR。网站在 Nginx/CDN 反代后面的话,先配好 real_ip 模块或让反代覆盖可信头,再取 REMOTE_ADDR(怎么在反代后面拿真实 IP 是个独立话题,这里不展开,只提醒别直接信客户端能伪造的 X-Forwarded-For)。
  2. 查归属地并匹配城市:调接口拿 prov/city,和事先配好的「城市 → 本地页」映射表比对。查不到城市(海外 IP、区县级数据缺失、接口超时)一律走默认页。
  3. 渲染:命中就渲染本地门店/区域活动页;没命中就渲染全国默认页。

两个工程细节提前说:归属地结果按 IP 缓存(文件或 Redis,缓存 6 小时够用,IP 属地一天内基本不变),别让每个访客都打一次上游接口;给搜索引擎放行默认页——让百度爬虫看到的是全国页而不是随机某个城市页,避免收录混乱(代码里用 UA 判断,示例只写了最简单的一种)。

三、PHP 实现

一个入口文件跑通,映射表按需扩充:

php
<?php
// index.php —— 按访客城市分发本地门店/区域活动页

// 城市 -> 本地页 映射表(city 字段与接口返回一致;更稳的做法是 prov+city 联合)
$regionMap = [
    '广州' => '/pages/gz-stores.php',   // 广州门店页
    '深圳' => '/pages/sz-stores.php',
    '成都' => '/pages/cd-stores.php',
    '杭州' => '/pages/hz-stores.php',
];

function clientIp(): string {
    // 直连取 REMOTE_ADDR;Nginx/CDN 反代请先配 real_ip 模块,仍取 REMOTE_ADDR
    return $_SERVER['REMOTE_ADDR'] ?? '';
}

function ipGeo(string $ip): array {
    // 6 小时文件缓存,避免每个访客都打上游接口(免费版 60 次/分钟/IP)
    $cacheFile = sys_get_temp_dir() . '/geo_' . md5($ip) . '.json';
    if (is_file($cacheFile) && (time() - filemtime($cacheFile) < 21600)) {
        $cached = json_decode((string)file_get_contents($cacheFile), true);
        if (is_array($cached)) return $cached;
    }
    $ctx = stream_context_create(['http' => [
        'timeout' => 3,
        'header' => "User-Agent: Mozilla/5.0 (compatible; RegionPage/1.0)\r\n", // 显式带UA,规避默认请求特征被拦
    ]]);
    $raw = @file_get_contents('https://ip9.com.cn/get?ip=' . urlencode($ip), false, $ctx);
    $geo = [];
    if ($raw !== false) {
        $resp = json_decode($raw, true);
        if (is_array($resp) && ($resp['ret'] ?? 0) === 200) {
            $geo = is_array($resp['data'] ?? null) ? $resp['data'] : [];
        }
    }
    @file_put_contents($cacheFile, json_encode($geo));   // 写失败不影响主流程
    return $geo;
}

function isSearchEngine(): bool {
    $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
    return preg_match('/Baiduspider|Googlebot|bingbot|Bytespider|Sogou/', $ua) === 1;
}

$geo   = ipGeo(clientIp());
$city  = (string)($geo['city'] ?? '');
$local = $regionMap[$city] ?? '';

if ($local !== '' && !isSearchEngine()) {
    include __DIR__ . $local;   // 命中:渲染本地门店/活动页
} else {
    include __DIR__ . '/pages/default.php';   // 未命中/爬虫:全国默认页
}

本地页示例(pages/gz-stores.php),把门店列表写进模板就行:

php
<?php
// pages/gz-stores.php —— 广州本地页
$stores = [
    ['name' => '广州天河店',   'addr' => '天河区体育西路 xx 号', 'tel' => '020-8888xxxx'],
    ['name' => '广州白云店',   'addr' => '白云区云城东路 xx 号', 'tel' => '020-6666xxxx'],
];
?>
<h1>广州门店</h1>
<ul>
<?php foreach ($stores as $s): ?>
    <li><?= htmlspecialchars($s['name']) ?><?= htmlspecialchars($s['addr']) ?><?= htmlspecialchars($s['tel']) ?></li>
<?php endforeach; ?>
</ul>

想改成「不同城市跳不同子域名/落地页」也简单,把 include 换成:

php
// header('Location: https://gz.example.com/', true, 302);
// exit;

两种形态的取舍见下节。代码就这么多,核心是「查一次归属地 + 查一次映射表」,没有第二种语言的实现;页面本身是什么框架(原生 PHP、模板引擎、WordPress)都不影响这套分发逻辑。

四、落地注意事项

  • 跳转 vs 软渲染,优先软渲染。302 跳转到不同 URL,会让「同一个页面」在不同城市访客眼里是不同网址,搜索引擎收录、分享出去的链接都会乱。同一域名下 include 不同内容块最稳;真要跳,只在有独立子站(gz.example.com)时用。
  • 爬虫必须放行默认页。上面代码用 UA 判断了主流搜索引擎。不做这步,百度收录的可能全是「广州店」页,其他城市的关键词全废。
  • 兜底顺序:查归属地失败、返回空城市、海外 IP,全部走默认页。海外访客你甚至可以再叠一层「按国家展示英文页」,那是另一个话题。
  • 别把经纬度当门店定位依据。接口返回的 lng/lat 是城市中心点,用来画地图中心可以,用来算「最近门店」会出笑话。最近门店要么用门店列表让用户自己选,要么上真正的定位。
  • 免费版没有 ip_type/区县字段(VIP 版提供)。做城市级分发用不到,不用纠结。
  • 缓存文件目录要可写;多台服务器部署时把缓存换 Redis/Memcached,避免每台机器各查一遍。

总结

官网按城市分发本地内容,服务端查一次 IP 归属地就解决了:https://ip9.com.cn/get?ip=<ip> 免注册返回省市,PHP 里 file_get_contents 加个浏览器 UA 就能用,匹配到城市就 include 对应的本地门店页。记住三条:软渲染优先于 302、爬虫放行默认页、查不到就走兜底页。接口字段和限流说明以官网 https://www.ip9.com.cn 为准,把映射表扩充到你的每个门店城市,这套逻辑能直接上线。