本文目录导读:

- 使用MySQL空间函数(最常用)
- GeoJSON + MySQL空间索引(推荐用于大数据)
- 使用Redis GEO(适合频繁查询)
- 类型化模型 + 自定义查询(ThinkPHP 6+)
- 分页和性能优化
- 数据库字段设计建议
- 选择建议
在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 + 精确计算 |
根据你的实际需求选择合适的方案,并注意为经纬度字段建立索引以提高查询性能。