首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 實現「附近的人」地理位置篩選與距離計算

     編輯:PHP 綜合知識

核心思路

「附近的人」功能通常分兩步:矩形邊界框初篩(利用經緯度範圍快速過濾)+ 精確球面距離計算(Haversine 公式)。這能在百萬級數據下保持毫秒級響應。

第一步:計算經緯度邊界框

給定中心點 ($lat, $lng) 和半徑 $distance(千米),計算四個頂點座標:

define('EARTH_RADIUS', 6371); // 地球平均半徑 km

function getBoundingBox($lat, $lng, $distance) {
    // 緯度跨度(度)
    $dlat = rad2deg($distance / EARTH_RADIUS);
    // 經度跨度(度),隨緯度變化
    $dlng = rad2deg(2 * asin(sin($distance / (2 * EARTH_RADIUS)) / cos(deg2rad($lat))));

    return [
        'left_top'     => ['lat' => $lat + $dlat, 'lng' => $lng - $dlng],
        'right_top'    => ['lat' => $lat + $dlat, 'lng' => $lng + $dlng],
        'left_bottom'  => ['lat' => $lat - $dlat, 'lng' => $lng - $dlng],
        'right_bottom' => ['lat' => $lat - $dlat, 'lng' => $lng + $dlng],
    ];
}

第二步:SQL 範圍查詢

假設表 users 有 lat, lng 字段(已建立聯合索引):

$box = getBoundingBox($centerLat, $centerLng, $radiusKm);

$sql = "SELECT * FROM `users` 
        WHERE `lat` BETWEEN {$box['left_bottom']['lat']} AND {$box['left_top']['lat']}
        AND `lng` BETWEEN {$box['left_top']['lng']} AND {$box['right_top']['lng']}
        AND `lat` != 0 AND `lng` != 0";

第三步:精確距離計算與排序

使用 Haversine 公式計算球面距離(單位:米):

function haversineDistance($lat1, $lng1, $lat2, $lng2) {
    $lat1 = deg2rad($lat1); $lng1 = deg2rad($lng1);
    $lat2 = deg2rad($lat2); $lng2 = deg2rad($lng2);

    $dlat = $lat2 - $lat1;
    $dlng = $lng2 - $lng1;

    $a = sin($dlat/2)**2 + cos($lat1) * cos($lat2) * sin($dlng/2)**2;
    $c = 2 * atan2(sqrt($a), sqrt(1-$a));

    return EARTH_RADIUS * $c * 1000; // 返回米
}

// 在 PHP 中對查詢結果二次排序
usort($results, function($a, $b) use ($centerLat, $centerLng) {
    $da = haversineDistance($centerLat, $centerLng, $a['lat'], $a['lng']);
    $db = haversineDistance($centerLat, $centerLng, $b['lat'], $b['lng']);
    return $da <=> $db;
});

性能優化建議

  1. 空間索引: MySQL 5.7+ 支持原生 SPATIAL INDEX,配合 ST_Distance_Sphere() 可下推到數據庫層完成。
  2. Redis Geo: 使用 GEOADD / GEORADIUS 實現毫秒級查詢,適合高併發場景。
  3. 分頁優化: 避免 OFFSET 深分頁,改用游標式分頁(記住上次最大距離/ID)。
  4. 緩存熱點: 熱門地標(商圈、地鐵站)預計算鄰居列表緩存至 Redis。

完整可用類封裝

class NearbySearch {
    const EARTH_RADIUS = 6371000; // 米

    public static function search(PDO $pdo, float $lat, float $lng, int $radius = 1000, int $limit = 20): array {
        // 1. 邊界框
        $dlat = rad2deg($radius / self::EARTH_RADIUS);
        $dlng = rad2deg(2 * asin(sin($radius / (2 * self::EARTH_RADIUS)) / cos(deg2rad($lat))));
        
        $minLat = $lat - $dlat; $maxLat = $lat + $dlat;
        $minLng = $lng - $dlng; $maxLng = $lng + $dlng;

        // 2. 粗篩 SQL
        $sql = "SELECT id, lat, lng, name FROM users 
                WHERE lat BETWEEN ? AND ? AND lng BETWEEN ? AND ? AND lat != 0 LIMIT 500";
        $stmt = $pdo->prepare($sql);
        $stmt->execute([$minLat, $maxLat, $minLng, $maxLng]);
        $candidates = $stmt->fetchAll(PDO::FETCH_ASSOC);

        // 3. 精確計算 + 排序
        foreach ($candidates as &$user) {
            $user['distance'] = self::haversine($lat, $lng, $user['lat'], $user['lng']);
        }
        usort($candidates, fn($a, $b) => $a['distance'] <=> $b['distance']);

        return array_slice($candidates, 0, $limit);
    }

    private static function haversine($lat1, $lng1, $lat2, $lng2): float {
        $lat1 = deg2rad($lat1); $lng1 = deg2rad($lng1);
        $lat2 = deg2rad($lat2); $lng2 = deg2rad($lng2);
        $dlat = $lat2 - $lat1; $dlng = $lng2 - $lng1;
        $a = sin($dlat/2)**2 + cos($lat1)*cos($lat2)*sin($dlng/2)**2;
        return 2 * atan2(sqrt($a), sqrt(1-$a)) * self::EARTH_RADIUS;
    }
}
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved