一. 开始介绍: Redis GEO
1.Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作,该功能在 Redis 3.2 版本新增。
Redis GEO | 操作方法 |
---|---|
geoadd | 添加地理位置的坐标 |
geopos | 获取地理位置的坐标 |
geodist | 计算两个位置之间的距离 |
georadius | 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合 |
georadiusbymember | 根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合 |
geohash | 返回一个或多个位置对象的 geohash 值 |
2.geoadd
geoadd 用于存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中。
3.geopos
geopos 用于从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回 nil。
4.geodist
geodist 用于返回两个给定位置之间的距离。
最后一个距离单位参数说明:
m :米,默认单位。
km :千米。
mi :英里。
ft :英尺。
5.georadius
georadius 以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
m :米,默认单位。
km :千米。
mi :英里。
ft :英尺。
WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。
WITHCOORD: 将位置元素的经度和维度也一并返回。
WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
COUNT 限定返回的记录数。
ASC: 查找结果根据距离从近到远排序。
DESC: 查找结果根据从远到近排序。
二. Redis6 + PHP 实现附近门店思路
1.首先在项目后台创建店铺的时候,一定要接通百度地图api,返回相对输入店铺地址的经纬度,保存到数据库后才能实现,没有经纬度无法实现 本人用的是laravel框架~~
百度地图api地址 : https://lbsyun.baidu.com/index.php?title=webapi
接下来是测试数据,如果对读者们有帮助,可以在实际项目中更具自己的思路加强改进~
如果之前创建店铺的时候保存了经纬度,这个数据可以在数据库中读取,id是门店的id,longitude 经度,latitude纬度
如果仔细的读者应该发现了,在这里用的语法是上文介绍过的,在这里只是把名称换成了店铺id,后面比较好和门店进行绑定
可以看到redis库中已经有我们填写的测试数据了,接下来我们结合上文所说到的georadius方法进行取值
这里使用的是asc升序排序,还有desc降序排序,一般附近门店第一个显示的是最近门店,所以用asc升序排序,单位是km 比较好查询
显示的是km单位,把数据拿到后可以 *1000 改成m单位,更具自己的项目需求进行改变
在这里遍历用店铺id做为数组的key
在这将门店距离数据和门店信息数据组合
最后组合成的数据,已经可以在前端小程序中渲染附近门店距离啦,这只是测试数据哦,真实数据要更具自己项目数据进行结合 对读者有帮助的话 帮我点个赞哦 期待留言~~
成品效果哦~~
(ps: 最后可以设防,读取数据库门店信息条数和redis 储存的门店经纬度数据总数 进行对比,如果一致,则直接读取redis数据,否则重新查询数据库,将数据重新赋值给redis,可以减少数据库io查询)
代码块
//模拟门店 id 经度 纬度
$store_location = array(
0 => array(
'id' => 1,
'longitude' => '113.956843',
'latitude' => '22.575094',
),
1 => array(
'id' => 2,
'longitude' => '113.957746',
'latitude' => '22.574877',
),
2 => array(
'id' => 3,
'longitude' => '113.957036',
'latitude' => '22.575545',
),
);
//遍历测试数据 循环在redis 插入 经纬度
foreach ($store_location as $K => $item) {
Redis::GeoAdd('store_location', $item['longitude'], $item['latitude'], $item['id']);
}
//在这里经纬度是写死的,在实际项目中这里传进来的经纬度是更具用户授权定位信息之后拿到的经纬度
$positions = Redis::GeoRadius('store_location', 113.955964, 22.574741, 1000, 'km', 'WITHDIST', 'asc');
//在这里遍历用店铺id做为数组key
$positions_container = [];
foreach ($positions as $k => $v) {
$positions_container[$v[0]] = $v;
}
//这里是门店信息测试数据,读者可以更具项目数据进行读取
$stores = array(
0 => array(
'id' => 1,
'name' => 'ck',
),
1 => array(
'id' => 2,
'name' => 'myj',
),
2 => array(
'id' => 3,
'name' => 'tyb',
),
);
//门店信息容器
$store_info = [];
foreach ($stores as $k => $v) { //遍历门店信息数据
foreach ($positions_container as $kk => $vv) { //遍历相对距离位置
//如果数组key等于门店信息id 则把数据重新赋值给门店信息容器 store_info
if ($kk = $v['id']) {
$store_info[$kk]['id'] = $v['id'];
$store_info[$kk]['name'] = $v['name'];
$store_info[$kk]['apart'] = ($positions_container[$kk][1] * 1) * 1000;
}
}
}
//取出门店距离数组列
$column = array_column($store_info, 'apart');
//更具距离进行升序排序
array_multisort($column, SORT_ASC, $store_info);
dd($store_info);
/**
* 获取附近线下门店列表
* @Author ZzzWClock
* @param $longitude 经度
* @param $latitude 纬度
* @method get
* @return json
*/
public function queryOfflineStore()
{
$longitude = req::get('longitude');
$latitude = req::get('latitude');
// $longitude = '113.955964';
// $latitude = '22.574741';
// 判断用户是否授权开启定位
if (empty($longitude) && empty($latitude)) {
return $this->resFailed(400, '请授权开启定位');
}
try {
$offline_store_info = offlineStore::checkPosition($longitude, $latitude);
return $this->resSuccess($offline_store_info);
} catch (\Exception $e) {
return $this->resFailed(400, $e->getMessage());
}
}
/**
* 根据经纬度获取附近门店
* @Author ZzzWClock
* @param $longitude 经度
* @param $latitude 纬度
* @param $data 分页/筛选数据
* @return array
*/
public static function checkPosition(String $longitude, String $latitude, array $data = [])
{
// 线下门店信息
$offline_store = self::queryOfflineStoreInfo($data);
// 判断是否有线下门店
if (!empty($offline_store)) {
// 线下门店经纬度 添加进redis
foreach ($offline_store as $key => $items) {
Redis::GeoAdd('offline_location', $items['latitude'], $items['longitude'], $items['id']);
}
// 用户经纬度放入redis中计算距离
$offline_location = Redis::GeoRadius('offline_location', $longitude, $latitude, 10000, 'km', 'WITHDIST', 'asc');
// 遍历门店根据距离进行升序排序
$store_info = self::eachOfflineLocation($offline_location, $offline_store);
return $store_info;
} else {
return $offline_store;
}
}
/**
* 查询线下门店信息
* @Author ZzzWClock
* @param $data 分页/筛选数据
* @return array
*/
private static function queryOfflineStoreInfo(array $data = [])
{
// 分页/筛选数据
// $data['page'] = $data['page'] ? $data['page'] : 1;
// $data['pageSize'] = $data['pageSize'] ? $data['pageSize'] : 10;
// 线下门店信息
$offline_store = DB::table('shop_real_stores')
->select([
'id', // 线下店铺id
'name', // 店铺名称
'brief', // 店铺描述
'mes', // 店铺信息
'province', // 省
'city', // 市
'area', // 区
'street', // 街道
'longitude', // 经度
'latitude', // 纬度
'distribution', // 配送距离
'business_hours', // 营业时间
'address', // 门店地址
'phone', // 门店电话
'image_url', // 图片地址
'longitude', // 经度
'latitude', // 纬度
])
->where(['status' => 1])
->lists();
return $offline_store;
}
/**
* 遍历线下门店经纬度索引 返回附近门店距离
* @Author ZzzWClock
* @param $offline_location redis门店距离
* @param $offline_store_info 线下门店信息
* @return array
*/
private static function eachOfflineLocation(array $param, array $offline_store_info)
{
// 遍历 redis 门店距离 更新索引
$positions_container = [];
foreach ($param as $k => $v) {
$positions_container[$v[0]] = $v;
}
// 遍历 门店信息
$store_info = [];
foreach ($offline_store_info as $k => $v) {
//二级遍历门店距离 通过key 找到相对应门店
foreach ($positions_container as $kk => $vv) {
if ($kk = $v['id']) {
$store_info[$k]['id'] = $v['id'];
$store_info[$k]['name'] = $v['name'];
$store_info[$k]['brief'] = $v['brief'];
$store_info[$k]['mes'] = $v['mes'];
$store_info[$k]['province'] = $v['province'];
$store_info[$k]['city'] = $v['city'];
$store_info[$k]['area'] = $v['area'];
$store_info[$k]['street'] = $v['street'];
$store_info[$k]['distribution'] = $v['distribution'];
$store_info[$k]['business_hours'] = json_decode($v['business_hours']);
$store_info[$k]['address'] = $v['address'];
$store_info[$k]['phone'] = $v['phone'];
$store_info[$k]['image_url'] = explode(',', $v['image_url']);
$store_info[$k]['apart'] = ($positions_container[$kk][1] * 1) * 1000;
$store_info[$k]['score'] = '5.0';
$store_info[$k]['longitude'] = $v['longitude'];
$store_info[$k]['latitude'] = $v['latitude'];
}
}
}
// 取出门店距离列
$apart_column = array_column($store_info, 'apart');
// 根据门店距离进行升序排序
array_multisort($apart_column, SORT_ASC, $store_info);
// 获取门店评分
$store_info = self::queryStoreRemark($store_info);
return $store_info;
}
/**
* 获取门店评分
* @Author ZzzWClock
* @param $offline_store_info 线下门店信息
* @return array
*/
private static function queryStoreRemark (array $offline_store_info)
{
// 获取门店id
$offline_store_ids = array_column($offline_store_info, 'id');
// 查询线下门店评分表
$store_remark = DB::table('shop_offline_store_remark')
->select([
'id',
'offline_store_id',
'total_score',
])
->where(['is_show' => 0])
->whereIn('id', $offline_store_ids)
->lists();
if (!empty($store_remark)) {
// 遍历线下门店信息 children 为评论数量
foreach ($offline_store_info as $k => $store_info) {
$offline_store_info[$k]['children'] = [];
foreach ($store_remark as $kk => $remark) {
if ($remark['offline_store_id'] == $store_info['id']) {
$offline_store_info[$k]['children'][] = $remark;
}
}
}
// 声明score_num 储存门店评分数量
$score_num = [];
foreach ($offline_store_info as $k => $store_info) {
$score_num[$store_info['id']][] = count($store_info['children']);
}
// 遍历线下门店信息评分 = 每个children 评分总合 / 门店评分数量
foreach ($offline_store_info as $k => $store_info) {
$total_score = 0;
foreach ($store_info['children'] as $kk => $cld) {
foreach ($score_num as $kkk => $num) {
if ($kkk == $cld['offline_store_id']) {
$offline_store_info[$k]['score'] = ($total_score += $cld['total_score']) / $num[0];
}
}
}
unset($offline_store_info[$k]['children']);
}
return $offline_store_info;
} else {
return $offline_store_info;
}
}