本文目录导读:

针对PHP项目中POI(兴趣点)周边地点检索的后端查询,我为你提供几种常见的技术方案和实现思路:
使用地图服务商API(推荐方案)
高德地图API示例
<?php
class POISearchService {
private $apiKey = '你的高德地图API Key';
/**
* 周边POI检索
* @param float $lat 纬度
* @param float $lng 经度
* @param int $radius 半径(米)
* @param string $keywords 关键词
* @param int $page 页码
* @return array
*/
public function searchAround($lat, $lng, $radius = 1000, $keywords = '', $page = 1) {
$url = 'https://restapi.amap.com/v3/place/around';
$params = [
'key' => $this->apiKey,
'location' => $lng . ',' . $lat,
'radius' => $radius,
'keywords' => $keywords,
'offset' => 20, // 每页记录数
'page' => $page,
'extensions' => 'all'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url . '?' . http_build_query($params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
/**
* 计算两点间距离(米)
*/
public function calculateDistance($lat1, $lng1, $lat2, $lng2) {
$earthRadius = 6371000; // 地球半径(米)
$latFrom = deg2rad($lat1);
$latTo = deg2rad($lat2);
$lngFrom = deg2rad($lng1);
$lngTo = deg2rad($lng2);
$latDelta = $latTo - $latFrom;
$lngDelta = $lngTo - $lngFrom;
$a = sin($latDelta/2) * sin($latDelta/2) +
cos($latFrom) * cos($latTo) *
sin($lngDelta/2) * sin($lngDelta/2);
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
return $earthRadius * $c;
}
}
// 使用示例
$poiService = new POISearchService();
$result = $poiService->searchAround(
39.9042, // 纬度
116.4074, // 经度
1000, // 半径1公里
'餐厅' // 关键词
);
print_r($result);
?>
百度地图API示例
<?php
class BaiduPOISearch {
private $ak = '你的百度地图AK';
public function searchAround($lat, $lng, $radius, $query) {
$url = 'https://api.map.baidu.com/place/v2/search';
$params = [
'ak' => $this->ak,
'output' => 'json',
'query' => $query,
'location' => $lat . ',' . $lng,
'radius' => $radius,
'page_size' => 20,
'page_num' => 0,
'scope' => 2
];
// 使用curl发起请求
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url . '?' . http_build_query($params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
}
?>
自建MySQL空间数据库查询
数据库表结构
CREATE TABLE poi_locations (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
address VARCHAR(500),
latitude DECIMAL(10, 7) NOT NULL,
longitude DECIMAL(10, 7) NOT NULL,
category VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
SPATIAL INDEX idx_location (latitude, longitude)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
PHP查询实现
<?php
class SelfPOISearch {
private $db;
public function __construct($host, $dbname, $user, $pass) {
$this->db = new PDO(
"mysql:host=$host;dbname=$dbname;charset=utf8mb4",
$user,
$pass
);
}
/**
* 使用Haversine公式计算距离
*/
public function searchByHaversine($lat, $lng, $radius, $limit = 20) {
$sql = "
SELECT
*,
(
6371 * acos(
cos(radians(:lat)) *
cos(radians(latitude)) *
cos(radians(longitude) - radians(:lng)) +
sin(radians(:lat2)) *
sin(radians(latitude))
)
) AS distance
FROM poi_locations
HAVING distance <= :radius
ORDER BY distance
LIMIT :limit
";
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':lat', $lat);
$stmt->bindParam(':lng', $lng);
$stmt->bindParam(':lat2', $lat);
$stmt->bindParam(':radius', $radius);
$stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 使用GeoHash优化查询
*/
public function searchByGeoHash($lat, $lng, $radius) {
// 计算经纬度范围
$latRange = $radius / 111000; // 1度纬度 ≈ 111km
$lngRange = $radius / (111000 * cos(deg2rad($lat)));
$minLat = $lat - $latRange;
$maxLat = $lat + $latRange;
$minLng = $lng - $lngRange;
$maxLng = $lng + $lngRange;
$sql = "
SELECT *,
get_distance(:lat, :lng, latitude, longitude) AS distance
FROM poi_locations
WHERE latitude BETWEEN :min_lat AND :max_lat
AND longitude BETWEEN :min_lng AND :max_lng
HAVING distance <= :radius
ORDER BY distance
";
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':lat', $lat);
$stmt->bindParam(':lng', $lng);
$stmt->bindParam(':min_lat', $minLat);
$stmt->bindParam(':max_lat', $maxLat);
$stmt->bindParam(':min_lng', $minLng);
$stmt->bindParam(':max_lng', $maxLng);
$stmt->bindParam(':radius', $radius);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
// 创建距离计算函数(MySQL)
$createFunction = "
CREATE FUNCTION get_distance(
lat1 DECIMAL(10,7),
lng1 DECIMAL(10,7),
lat2 DECIMAL(10,7),
lng2 DECIMAL(10,7)
) RETURNS DECIMAL(10,2)
DETERMINISTIC
BEGIN
DECLARE R DECIMAL(10,5) DEFAULT 6371;
DECLARE dLat DECIMAL(20,10);
DECLARE dLng DECIMAL(20,10);
DECLARE a DECIMAL(20,10);
DECLARE c DECIMAL(20,10);
SET dLat = RADIANS(lat2 - lat1);
SET dLng = RADIANS(lng2 - lng1);
SET a = POW(SIN(dLat/2), 2) +
COS(RADIANS(lat1)) * COS(RADIANS(lat2)) *
POW(SIN(dLng/2), 2);
SET c = 2 * ATAN2(SQRT(a), SQRT(1-a));
RETURN R * c * 1000; -- 返回米
END
";
?>
使用Elasticsearch地理位置查询
Elasticsearch索引设置
<?php
class ElasticPOISearch {
private $client;
public function __construct() {
// 使用Elasticsearch PHP客户端
$hosts = ['http://localhost:9200'];
$this->client = Elasticsearch\ClientBuilder::create()
->setHosts($hosts)
->build();
}
/**
* 创建索引映射
*/
public function createIndex() {
$params = [
'index' => 'poi_locations',
'body' => [
'mappings' => [
'properties' => [
'name' => ['type' => 'text'],
'location' => ['type' => 'geo_point'],
'category' => ['type' => 'keyword'],
'rating' => ['type' => 'float']
]
]
]
];
return $this->client->indices()->create($params);
}
/**
* 周边POI搜索
*/
public function searchAround($lat, $lng, $radius, $query = '') {
$params = [
'index' => 'poi_locations',
'body' => [
'query' => [
'bool' => [
'must' => [
'geo_distance' => [
'distance' => $radius . 'm',
'location' => [
'lat' => $lat,
'lon' => $lng
]
]
],
'should' => [
'match' => [
'name' => $query
]
]
]
],
'sort' => [
'_geo_distance' => [
'location' => [
'lat' => $lat,
'lon' => $lng
],
'order' => 'asc',
'unit' => 'm'
]
]
]
];
$response = $this->client->search($params);
return $response['hits']['hits'];
}
}
?>
性能优化建议
缓存策略
<?php
class POICache {
private $redis;
public function __construct() {
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
}
/**
* 带缓存的POI查询
*/
public function getCachedPOI($lat, $lng, $radius) {
// 生成缓存key
$cacheKey = "poi:{$lat}:{$lng}:{$radius}";
// 检查缓存
if ($this->redis->exists($cacheKey)) {
return json_decode($this->redis->get($cacheKey), true);
}
// 查询数据库
$poiService = new SelfPOISearch('localhost', 'poi_db', 'user', 'pass');
$result = $poiService->searchByHaversine($lat, $lng, $radius);
// 设置缓存(5分钟过期)
$this->redis->setex($cacheKey, 300, json_encode($result));
return $result;
}
}
?>
数据库优化
-- 添加复合索引
ALTER TABLE poi_locations ADD INDEX idx_category_location (category, latitude, longitude);
-- 使用分区表
CREATE TABLE poi_locations_partitioned (
-- ... 字段定义
) ENGINE=InnoDB
PARTITION BY RANGE (id) (
PARTITION p0 VALUES LESS THAN (100000),
PARTITION p1 VALUES LESS THAN (200000),
PARTITION p2 VALUES LESS THAN (300000),
PARTITION p3 VALUES LESS THAN MAXVALUE
);
完整的API接口示例
<?php
// api/poi_search.php
header('Content-Type: application/json; charset=utf-8');
// 验证请求参数
$lat = isset($_GET['lat']) ? floatval($_GET['lat']) : 0;
$lng = isset($_GET['lng']) ? floatval($_GET['lng']) : 0;
$radius = isset($_GET['radius']) ? intval($_GET['radius']) : 1000;
$type = isset($_GET['type']) ? $_GET['type'] : 'food';
$page = isset($_GET['page']) ? intval($_GET['page']) : 1;
// 参数验证
if ($lat == 0 || $lng == 0) {
echo json_encode(['code' => 400, 'message' => '缺少经纬度参数']);
exit;
}
try {
// 选择搜索方式(配置文件定义)
$searchType = 'baidu'; // 或 'self', 'elastic'
switch($searchType) {
case 'baidu':
$search = new BaiduPOISearch();
$result = $search->searchAround($lat, $lng, $radius, $type);
break;
case 'self':
$search = new SelfPOISearch('localhost', 'poi_db', 'user', 'pass');
$result = $search->searchByHaversine($lat, $lng, $radius);
break;
case 'elastic':
$search = new ElasticPOISearch();
$result = $search->searchAround($lat, $lng, $radius);
break;
}
// 格式化输出
$response = [
'code' => 200,
'data' => [
'total' => count($result),
'pois' => formatPOIResult($result),
'page' => $page
],
'request' => [
'lat' => $lat,
'lng' => $lng,
'radius' => $radius,
'type' => $type
]
];
echo json_encode($response);
} catch (Exception $e) {
echo json_encode([
'code' => 500,
'message' => '查询失败: ' . $e->getMessage()
]);
}
function formatPOIResult($data) {
$formatted = [];
foreach ($data as $poi) {
$formatted[] = [
'id' => $poi['id'],
'name' => $poi['name'],
'address' => $poi['address'] ?? '',
'lat' => $poi['latitude'],
'lng' => $poi['longitude'],
'distance' => round($poi['distance'] ?? 0, 2),
'category' => $poi['category'] ?? ''
];
}
return $formatted;
}
?>
选择建议
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 地图服务商API | 快速开发,数据量小 | 数据准确,部署简单 | 有调用次数限制 |
| 自建数据库 | 数据量大,定制化需求 | 完全控制,无费用 | 维护成本高 |
| Elasticsearch | 大数据量,高并发 | 查询快,扩展性好 | 学习成本高 |
根据你的项目规模、数据量和服务要求选择合适方案,对于小型项目,推荐使用地图服务商API;对于大型商业项目,推荐Elasticsearch方案。