本文目录导读:

- 使用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 + 精确计算 |
根据你的实际需求选择合适的方案,并注意为经纬度字段建立索引以提高查询性能。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播