ThinkPHP项目地理位置查询

wen PHP项目 3

本文目录导读:

ThinkPHP项目地理位置查询

  1. 使用MySQL空间函数(最常用)
  2. GeoJSON + MySQL空间索引(推荐用于大数据)
  3. 使用Redis GEO(适合频繁查询)
  4. 类型化模型 + 自定义查询(ThinkPHP 6+)
  5. 分页和性能优化
  6. 数据库字段设计建议
  7. 选择建议

在ThinkPHP中进行地理位置查询,主要有以下几种常见方案:

使用MySQL空间函数(最常用)

基础经纬度查询

<?php
namespace app\common\service;
use think\facade\Db;
class LocationService
{
    /**
     * 计算两点之间的距离
     */
    public function calculateDistance($lat1, $lng1, $lat2, $lng2)
    {
        $earthRadius = 6371; // 地球半径(公里)
        $dLat = deg2rad($lat2 - $lat1);
        $dLng = deg2rad($lng2 - $lng1);
        $a = sin($dLat/2) * sin($dLat/2) + 
             cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * 
             sin($dLng/2) * sin($dLng/2);
        $c = 2 * atan2(sqrt($a), sqrt(1-$a));
        return $earthRadius * $c; // 返回距离(公里)
    }
    /**
     * 查询指定范围内的用户
     * @param float $lat 纬度
     * @param float $lng 经度
     * @param int $radius 半径(公里)
     */
    public function findNearbyUsers($lat, $lng, $radius = 10)
    {
        // 使用MySQL的Haversine公式
        $users = Db::name('users')
            ->field("*, (6371 * acos(cos(radians({$lat})) 
                    * cos(radians(latitude)) 
                    * cos(radians(longitude) - radians({$lng})) 
                    + sin(radians({$lat})) 
                    * sin(radians(latitude)))) AS distance")
            ->where("latitude", ">", $lat - $radius / 111.0)
            ->where("latitude", "<", $lat + $radius / 111.0)
            ->where("longitude", ">", $lng - $radius / (111.0 * cos(deg2rad($lat))))
            ->where("longitude", "<", $lng + $radius / (111.0 * cos(deg2rad($lat))))
            ->having('distance <= ' . $radius)
            ->order('distance ASC')
            ->select()
            ->toArray();
        return $users;
    }
    /**
     * 使用MySQL内置的ST_Distance_Sphere(MySQL 5.7+)
     */
    public function findNearbyUsersV2($lat, $lng, $radius = 10)
    {
        $radiusInMeters = $radius * 1000; // 转换公里为米
        $users = Db::name('users')
            ->field("*, ST_Distance_Sphere(
                POINT(longitude, latitude),
                POINT({$lng}, {$lat})
            ) AS distance_m")
            ->having('distance_m <= ' . $radiusInMeters)
            ->order('distance_m ASC')
            ->select()
            ->toArray();
        return $users;
    }
}
?>

GeoJSON + MySQL空间索引(推荐用于大数据)

创建空间索引

-- 确保字段类型为POINT
ALTER TABLE users ADD location POINT;
CREATE SPATIAL INDEX idx_location ON users(location);
<?php
class GeoService
{
    /**
     * 插入地理位置数据
     */
    public function addLocation($userId, $lat, $lng)
    {
        Db::execute("UPDATE users SET location = POINT({$lng}, {$lat}) WHERE id = {$userId}");
    }
    /**
     * 使用空间索引查询
     */
    public function findNearbyV2($lat, $lng, $radiusKm)
    {
        // 计算边界框
        $latDelta = $radiusKm / 111.32;
        $lngDelta = $radiusKm / (111.32 * cos(deg2rad($lat)));
        $minLat = $lat - $latDelta;
        $maxLat = $lat + $latDelta;
        $minLng = $lng - $lngDelta;
        $maxLng = $lng + $lngDelta;
        $users = Db::query("
            SELECT *, 
                   ST_Distance_Sphere(POINT(longitude, latitude), POINT({$lng}, {$lat})) AS distance
            FROM users
            WHERE ST_Within(location, ST_MakeEnvelope(
                POINT({$minLng}, {$minLat}),
                POINT({$maxLng}, {$maxLat})
            ))
            AND ST_Distance_Sphere(POINT(longitude, latitude), POINT({$lng}, {$lat})) <= {$radiusKm} * 1000
            ORDER BY distance ASC
        ");
        return $users;
    }
}
?>

使用Redis GEO(适合频繁查询)

<?php
namespace app\common\service;
use think\facade\Cache;
class RedisGeoService
{
    private $redis;
    public function __construct()
    {
        $this->redis = Cache::store('redis')->handler();
    }
    /**
     * 添加地理位置
     */
    public function addLocation($key, $userId, $lat, $lng)
    {
        $this->redis->geoadd($key, $lng, $lat, $userId);
    }
    /**
     * 搜索附近的人
     */
    public function searchNearby($key, $lat, $lng, $radius = 10, $unit = 'km')
    {
        // 搜索附近的人
        $result = $this->redis->geosearch(
            $key,
            ['coord' => [$lng, $lat]],
            ['radius' => $radius, 'unit' => $unit],
            ['withcoord' => true, 'withdist' => true, 'sort' => 'asc']
        );
        // 获取用户详细信息
        $users = [];
        foreach ($result as $userId => $info) {
            $userInfo = Db::name('users')->find($userId);
            $userInfo['distance'] = $info[1];
            $users[] = $userInfo;
        }
        return $users;
    }
}
?>

类型化模型 + 自定义查询(ThinkPHP 6+)

<?php
namespace app\model;
use think\Model;
class User extends Model
{
    // 获取附近的用户
    public function scopeNearby($query, $lat, $lng, $radius = 10)
    {
        // Haversine公式
        $formula = "(
            6371 * acos(cos(radians({$lat})) 
            * cos(radians(latitude)) 
            * cos(radians(longitude) - radians({$lng})) 
            + sin(radians({$lat})) 
            * sin(radians(latitude)))
        )";
        return $query
            ->field("*")
            ->fieldRaw("{$formula} as distance")
            ->whereRaw("{$formula} <= {$radius}")
            ->order('distance ASC');
    }
}
// 控制器中使用
$users = User::nearby($lat, $lng, 5)->select();
?>

分页和性能优化

<?php
class OptimizedGeoService
{
    /**
     * 优化的查询(带分页和缓存)
     */
    public function getNearbyUsers($lat, $lng, $radius = 10, $page = 1, $limit = 20)
    {
        // 缓存key
        $cacheKey = "users:geo:{$lat}:{$lng}:{$radius}:{$page}";
        // 尝试从缓存读取
        $cached = Cache::get($cacheKey);
        if ($cached !== null) {
            return $cached;
        }
        // 只查询指定字段,避免查询所有字段
        $users = Db::name('users')
            ->field('id, name, avatar, latitude, longitude')
            ->where('latitude', 'between', [$lat - 0.09, $lat + 0.09])
            ->where('longitude', 'between', [$lng - 0.09, $lng + 0.09])
            ->page($page, $limit * 2) // 多取一些用于过滤
            ->select()
            ->toArray();
        // 计算实际距离并过滤
        $result = array_filter($users, function($user) use ($lat, $lng, $radius) {
            $distance = $this->haversineDistance($lat, $lng, $user['latitude'], $user['longitude']);
            $user['distance'] = $distance;
            return $distance <= $radius;
        });
        // 按距离排序
        usort($result, function($a, $b) {
            return $a['distance'] <=> $b['distance'];
        });
        // 缓存结果(5分钟有效)
        Cache::set($cacheKey, array_slice($result, 0, $limit), 300);
        return array_slice($result, 0, $limit);
    }
    private function haversineDistance($lat1, $lng1, $lat2, $lng2)
    {
        $earthRadius = 6371;
        $dLat = deg2rad($lat2 - $lat1);
        $dLng = deg2rad($lng2 - $lng1);
        $a = sin($dLat/2) * sin($dLat/2) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng/2) * sin($dLng/2);
        $c = 2 * atan2(sqrt($a), sqrt(1-$a));
        return $earthRadius * $c;
    }
}
?>

数据库字段设计建议

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL,
  `latitude` decimal(10, 6) NOT NULL COMMENT '纬度',
  `longitude` decimal(10, 6) NOT NULL COMMENT '经度',
  `location` point DEFAULT NULL COMMENT '空间位置',
  PRIMARY KEY (`id`),
  KEY `idx_lat_lng` (`latitude`, `longitude`),
  SPATIAL INDEX `idx_location` (`location`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

选择建议

场景 推荐方案
小型项目/简单查询 MySQL Haversine公式
大数据量/高性能 MySQL空间索引或Redis GEO
实时性要求高 Redis GEO
离线分析 ElasticSearch + Geo
通用型 MySQL + 精确计算

根据你的实际需求选择合适的方案,并注意为经纬度字段建立索引以提高查询性能。

抱歉,评论功能暂时关闭!