首页 > 其他分享 >社交--附近的人

社交--附近的人

时间:2024-06-12 15:00:51浏览次数:13  
标签:diner -- 位置 lon 用户 location lat 社交 附近

1. 需求分析
各种社交软件基本都有附近的人的需求,该APP查询附近1公里食客,同时只需要查20个即可


2. 设计思路
解决基于地理位置的搜索,MySQL、MongoDB、Redis 都支持地理位置的存储

“附近的人” 也就是常说的 LBS (Location Based Services,基于位置服务),它围绕用户当前地理位置数据而展开的服务,为用户提供精准的增值服务。

以 “我” 为中心,搜索附近的用户
以 “我” 当前的地理位置为准,计算出别人和 “我” 之间的距离
按 “我” 与别人距离的远近排序,筛选出离我最近的用户或者商店等
当用户登录应用时,或者保持用户登录后用户在使用应用时,客户端是可以时刻获取用户位置信息的(前提是用户要开启位置获取的权限),客户端获取到最新的地理位置后,上传到后端服务器进行更新。
当用户点击Near Me功能时,那么通过后台就可以以当前用户的位置为圆点,距离为半径查询相关的用户展示即可完成
3. Redis GEO常用指令


GEOADD
GEOADD key longitude latitude member [longitude latitude member …] 添加位置信息

# 添加单个位置
127.0.0.1:0>GEOADD diner:location 121.446617 31.205593 'zhangsan'
"1"
# 添加多个位置信息
127.0.0.1:0>GEOADD diner:location 121.4465774 31.20485103 'lisi' 121.44534
31.2031 'wangwu' 121.4510648 31.2090667 'zhangliu'
"3"
1
2
3
4
5
6
7
GEODIST
GEODIST key member1 member2 [unit] 计算距离, 其中unit为单位 m|km|ft(英尺)|mi(英里)

# 计算两点间的距离,返回距离的单位是米(m)
127.0.0.1:0>GEODIST diner:location zhangsan lisi m
"82.4241"
# 计算两点间的距离,返回距离的单位是千米(km)
127.0.0.1:0>GEODIST diner:location zhangsan lisi km
"0.0824"
1
2
3
4
5
6
GEOHASH
GEOHASH key member [member …] 返回一个或多个位置元素的Geohash。保存到Redis中是用Geohash位置52点整数编码。
GeoHash将二维的经纬度转换成字符串,比如下图展示了北京9个区域的GeoHash字符串,分别是WX4ER,WX4G2、WX4G3等,每一个字符串代表了某一矩形区域。也就是说,这个矩形区域内所有的点(经纬度坐标)都共享相同的GeoHash字符串,这样既可以保护隐私(只表示大概区域位置而不是具体的点),又比较容易做缓存,比如左上角这个区域内的用户不断发送位置信息请求餐馆数据,由于这些用户的GeoHash字符串都是WX4ER,所以可以把WX4ER当作key,把该区域的餐馆信息当作value来进行缓存,而如果不使用GeoHash的话,由于区域内的用户传来的经纬度是各不相同的,很难做缓存。字符串越长,表示的范围越精确

http://openlocation.org/geohash/geohash-js/ 提供了在地图上显示geohash编码的功能

# 计算某个位置的GeoHash值
127.0.0.1:0>GEOHASH diner:location zhangsan
1) "wtw3e8f9z20"
1
2
3
GEOPOS
GEOPOS key member [member …]从 key 里返回所有给定位置元素的位置(经度和纬度 )

# 返回zhangsan和lisi的位置信息
127.0.0.1:0>GEOPOS diner:location zhangsan lisi
1) 1) "121.44661813974380493"
2) "31.20559220971455971"
2) 1) "121.44657522439956665"
2) "31.20485207113603821"
1
2
3
4
5
6
GEORADIUS
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
范围可以使用以下其中一个单位:

m 表示单位为米。
km 表示单位为千米。
mi 表示单位为英里。
ft 表示单位为英尺。
在给定以下可选项时, 命令会返回额外的信息:

WITHDIST : 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。
WITHCOORD : 将位置元素的经度和维度也一并返回。
WITHHASH : 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。这个选项主要用于底层应用或者调试, 实际中的作用并不大。
命令默认返回未排序的位置元素。 通过以下两个参数, 用户可以指定被返回位置元素的排序方式:

ASC : 根据中心的位置, 按照从近到远的方式返回位置元素。
DESC : 根据中心的位置, 按照从远到近的方式返回位置元素。
在默认情况下, GEORADIUS 命令会返回所有匹配的位置元素。 虽然用户可以使用 COUNT 选项去获取前 N 个匹配元素, 但是因为命令在内部可能会需要对所有被匹配的元素进行处理, 所以在对一个非常大的区域进行搜索时, 即使只使用 COUNT 选项去获取少量元素, 命令的执行速度也可能会非常慢。但是从另一方面来说, 使用 COUNT 选项去减少需要返回的元素数量, 对于减少带宽来说仍然是非常有用的

# 以121.446617 31.205593(张三位置)为圆心,3000m为半径,查询返回用户及其位置
127.0.0.1:0>GEORADIUS diner:location 121.446617 31.205593 3000 m WITHCOORD
1) 1) "wangwu"
2) 1) "121.44534140825271606"
2) "31.20310057881493293"
2) 1) "lisi"
2) 1) "121.44657522439956665"
2) "31.20485207113603821"
3) 1) "zhangsan"
2) 1) "121.44661813974380493"
2) "31.20559220971455971"
4) 1) "zhangliu"
2) 1) "121.45106524229049683"
2) "31.20906731242401833"


# 以121.446617 31.205593(张三位置)为圆心,3000m为半径,查询返回用户及其距离(单位是米)
127.0.0.1:0>GEORADIUS diner:location 121.446617 31.205593 3000 m WITHDIST
1) 1) "wangwu"
2) "302.6202"
2) 1) "lisi"
2) "82.5066"
3) 1) "zhangsan"
2) "0.1396"
4) 1) "zhangliu"
2) "573.0651"
# 以121.446617 31.205593(张三位置)为圆心,3000m为半径,查询返回用户及其距离(单位是米) 由近
及远
47.110.246.98:15>GEORADIUS diner:location 121.446617 31.205593 3000 m WITHDIST
ASC
1) 1) "zhangsan"
2) "0.1396"
2) 1) "lisi"
2) "82.5066"
3) 1) "wangwu"
2) "302.6202"
4) 1) "zhangliu"
2) "573.0651"
# 以121.446617 31.205593(张三位置)为圆心,3000m为半径,查询返回用户及其GeoHash值
127.0.0.1:0>GEORADIUS diner:location 121.446617 31.205593 3000 m WITHHASH
1) 1) "wangwu"
2) "4054756135204337"
2) 1) "lisi"
2) "4054756138536712"
3) 1) "zhangsan"
2) "4054756138736536"
4) 1) "zhangliu"
2) "4054756186304127"
# 以121.446617 31.205593(张三位置)为圆心,3000m为半径,查询返回用户及其GeoHash值去2个
127.0.0.1:0>GEORADIUS diner:location 121.446617 31.205593 3000 m WITHHASH COUNT
2
1) 1) "zhangsan"
2) "4054756138736536"
2) 1) "lisi"
2) "4054756138536712"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
GEORADIUSBYMEMBER
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH]
[COUNT count],这个命令和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是
GEORADIUSBYMEMBER 的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS 那样, 使用输入的经度和纬度来决定中心点指定成员的位置被用作查询的中心。

原理`
GEOADD添加地理位置信息时候, 经度必须在纬度前

有效的经度从-180度到180度。
有效的纬度从-85.05112878度到85.05112878度。
当坐标位置超出上述指定范围时,该命令将会返回一个错误。

如何工作
sorted set使用一种称为Geohash的技术进行填充。经度和纬度的位是交错的,以形成一个独特的52位整数. 我们知道,一个sorted set 的double score可以代表一个52位的整数,而不会失去精度。
这种格式允许半径查询检查的1 + 8个领域需要覆盖整个半径,并丢弃元素以外的半径。通过计算该区域的范围,通过计算所涵盖的范围,从不太重要的部分的排序集的得分,并计算得分范围为每个区域的sorted set中的查询。

4. 上传用户位置
在 fs-diners 服务中编写功能

RedisKeyConstant
diner_location("diner:location", "diner地理位置信息"),
1
NearMeService
保存的key为:diner:location, member为dinerId

/**
* 附近的人 Service
*/
@Service
public class NearMeService {

@Value("${service.name.fs-oauth-server}")
private String oauthServerName;
@Resource
private RestTemplate restTemplate;
@Resource
private RedisTemplate redisTemplate;
@Resource
private com.itkaka.diners.service.DinersService dinersService;

/**
* 更新食客坐标
*
* @param accessToken 当前登录用户 token
* @param lon 经度
* @param lat 纬度
*/
public void updateDinerLocation(String accessToken, Float lon, Float lat) {
// 参数校验
AssertUtil.isTrue(lon == null, "获取经度失败");
AssertUtil.isTrue(lat == null, "获取纬度失败");
// 获取登录用户信息
SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
// 构建 Redis Key
String key = RedisKeyConstant.diner_location.getKey();
// 将用户地理位置存入 Redis
Integer dinerId = dinerInfo.getId();
RedisGeoCommands.GeoLocation geoLocation = new RedisGeoCommands
.GeoLocation(dinerId, new Point(lon, lat));
redisTemplate.opsForGeo().add(key, geoLocation);
}

/**
* 获取登录用户信息
*
* @param accessToken
* @return
*/
private SignInDinerInfo loadSignInDinerInfo(String accessToken) {
// 是否有 accessToken
AssertUtil.mustLogin(accessToken);
// 拼接远程请求 url
String url = oauthServerName + "user/me?access_token={accessToken}";
// 发送请求
ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class, accessToken);
if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
throw new ParameterException(resultInfo.getCode(), resultInfo.getMessage());
}
SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(), new SignInDinerInfo(), false);
return dinerInfo;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
NearMeController
传入登录用户的lon(精度)和lat(维度)信息,一般根据实际情况,客户端要定时去获取用户的地理位置进行上传(5s中一般刷新一次)

/**
* 附近的人 Controller
*/
@RestController
@RequestMapping("nearme")
public class NearMeController {

@Resource
private NearMeService nearMeService;
@Resource
private HttpServletRequest request;

/**
* 更新食客坐标
*
* @param access_token
* @param lon
* @param lat
* @return
*/
@PostMapping
public ResultInfo updateDinerLocation(String access_token,
@RequestParam Float lon,
@RequestParam Float lat) {
nearMeService.updateDinerLocation(access_token, lon, lat);
return ResultInfoUtil.buildSuccess(request.getServletPath(), "更新成功");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
测试
访问:http://localhost/diners/nearme?access_token=116af711-645c-4ce9-bc32-cd304bdda020

 

使用 JMeter 压测并插入多条数据

 


5. 查找附近的人
传入登录用户token,同时传入查询范围(默认1000m)以及当前用户的lon(经),lat(纬)度,为什么要传入用户此时的经纬度呢,这样查出来的结果更加准确。有可能用户处于移动状态。

封装返回的VO
package com.itkaka.diners.model.vo;

import com.itkaka.commons.model.vo.ShortDinerInfo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;

@ApiModel(description = "附近的人")
@Getter
@Setter
public class NearMeDinerVO extends ShortDinerInfo {

@ApiModelProperty(value = "距离", example = "50m")
private String distance;

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Service的查询方法
获取登录用户id
获取查询半径,以米为单位,默认1000m
获取用户的经纬度,如果客户端没上传经纬度,那么从Redis中读取经纬度
格式化查询的半径,使用RedisTemplate的Distance对象
查询限制条件:限制20,返回包含距离,按由近及远排序
格式化结果,将其封装到Map中,Key为dinerId,Value构建返回的VO,同时格式化distance属性,方便客户端展示
查询附近的人的信息,并添加到对应的VO中
返回结果
/**
* 获取附近的人
*
* @param accessToken 当前登录用户 token
* @param radius 半径(m),默认 1000m
* @param lon 经度
* @param lat 纬度
* @return
*/
public List<NearMeDinerVO> findNearMe(String accessToken, Integer radius,
Float lon, Float lat) {
// 获取登录用户信息
SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
// 食客 ID
Integer dinerId = dinerInfo.getId();
// 查询半径,默认 1000m
if (radius == null) {
radius = 1000;
}
// 构建 Redis Key
String key = RedisKeyConstant.diner_location.getKey();
// 获取用户经纬度
Point point = null;
if (lon == null || lat == null) {
// 如果经纬度没传,那么从 Reids 中获取,但客户端传入会比较精确
List<Point> pointList = redisTemplate.opsForGeo().position(key, dinerId);
AssertUtil.isTrue(pointList == null || pointList.isEmpty(), "获取经纬度失败");
point = pointList.get(0);
} else {
point = new Point(lon, lat);
}
// 初始化距离对象,单位 m
Distance distance = new Distance(radius, RedisGeoCommands.DistanceUnit.METERS);
// 初始化 GEO 命令参数对象
RedisGeoCommands.GeoRadiusCommandArgs args =
RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs();
// 附近的人限制 20,包含距离,按由近到远排序
args.limit(20).includeDistance().sortAscending();
// 以当前登录用户经纬度为圆心,范围 1000m
Circle circle = new Circle(point, distance);
// 获取附近的人信息
GeoResults<RedisGeoCommands.GeoLocation<Integer>> results =
redisTemplate.opsForGeo().radius(key, circle, args);
// 构建有序 Map
Map<Integer, NearMeDinerVO> nearMeDinerVOMap = Maps.newLinkedHashMap();
// 循环处理距离信息
results.forEach(result -> {
// 获取 locationName 也就是食客 ID
RedisGeoCommands.GeoLocation<Integer> location = result.getContent();
// 初始化 VO 对象
NearMeDinerVO nearMeDinerVO = new NearMeDinerVO();
nearMeDinerVO.setId(location.getName());
// 格式化距离
double dist = result.getDistance().getValue();
// 四舍五入精确到小数点 1 位,为了方便客户端显示
// 这里后期需要扩展处理,根据距离显示 m 或者 km
String distanceStr = NumberUtil.roundStr(dist, 1) + "m";
nearMeDinerVO.setDistance(distanceStr);
nearMeDinerVOMap.put(location.getName(), nearMeDinerVO);
});
// 完善附近的人信息
Integer[] dinerIds = nearMeDinerVOMap.keySet().toArray(new Integer[]{});
List<ShortDinerInfo> shortDinerInfos = dinersService.findByIds(StrUtil.join(",", dinerIds));
shortDinerInfos.forEach(shortDinerInfo -> {
NearMeDinerVO nearMeDinerVO = nearMeDinerVOMap.get(shortDinerInfo.getId());
nearMeDinerVO.setNickname(shortDinerInfo.getNickname());
nearMeDinerVO.setAvatarUrl(shortDinerInfo.getAvatarUrl());
});
return Lists.newArrayList(nearMeDinerVOMap.values());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
Controller层方法
/**
* 获取附近的人
*
* @param access_token
* @param radius
* @param lon
* @param lat
* @return
*/
@GetMapping
public ResultInfo<List<NearMeDinerVO>> nearMe(String access_token, Integer radius,
Float lon, Float lat) {
List<NearMeDinerVO> result = nearMeService.findNearMe(access_token, radius, lon, lat);
return ResultInfoUtil.buildSuccess(request.getServletPath(), result);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Postman测试
访问:http://localhost/diners/nearme?access_token=116af711-645c-4ce9-bc32-cd304bdda020


6. 本文小结
附近的人

这个功能中我们实现了上传用户坐标、获取附近的人功能。 这个功能中 Redis 主要用于存储地理位置信息,使用了 GEO 数据类型

7. 拓展思考
基于MySQL实现附近的人,建一张表记录经纬度,然后通过计算得到两个点的距离(比如spatial4j等等),然后对距离结果排序;
设计一张表存用户的经、纬度信息,但区别是要多一个geo_code字段,存放GeoHash字符串,此字段通过用户经、纬度属性计算出。使用频繁的字段建议加上索引。首先根据用户经、纬度信息,在指定精度后计算用户坐标的GeoHash码,再获取到用户周边8个方位的GeoHash码在数据库中搜索用户,最后过滤掉超出给定距离(500米内)的用户
MongoDB + 2d索引
地理空间索引 2dsphere 和 2d,底层依然是基于GeoHash;
首先 插入一批位置数据到MongoDB, collection为起名,相当于MySQL的表名。两个字段name名称,location 为经、纬度数据对;
然后 location 字段创建一个2d索引,索引的精度通过bits来指定,bits越大,索引的精度就越高;
最后, 用geoNear命令测试一下, near 当前坐标(经、纬度),spherical 是否计算球面距离,distanceMultiplier地球半径,单位是米,默认6378137, maxDistance 过滤条件(指定距离内的用户),开启弧度需除distanceMultiplier,distanceField 计算出的两点间距离,字段别名,
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/Kaka_csdn14/article/details/131033803

标签:diner,--,位置,lon,用户,location,lat,社交,附近
From: https://www.cnblogs.com/gaoyanbing/p/18243956

相关文章

  • sql字段对应批量数据处理工具
    <htmllang="zh-CN"><head> <metacharset="utf-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width=device-width,initial-scal......
  • pytorch--Matrix相关
    pytorch–Matrix相关1.矩阵生成Tensor,即张量,是PyTorch中的基本操作对象,可以看做是包含单一数据类型元素的多维矩阵。从使用角度来看,Tensor与NumPy的ndarrays非常类似,相互之间也可以自由转换,只不过Tensor还支持GPU的加速。1.1创建一个没有初始化的矩阵x=torch.empty(2,......
  • 大模型三种架构
    大模型进化树灰色代表其他模型粉色表示encoder-only绿色代表encoder-decoder蓝色代表decoder-only1.encoder-only代表的有google的bert模型。专注于理解和编码输入信息,常用于分类、标注等任务优点:强大的理解能力:能够有效处理和理解输入数据。缺点:生成能力有限:不擅......
  • 细胞计数算法 —— 软件与平台实现
    细胞计数算法(自动识别细胞并统计存活)——软件与平台实现结果展示项目介绍项目背景算法详解结果展示原图识别效果图项目介绍项目分为软件和平台双形态。软件可发送邮件至[email protected]处获取,平台可经由网址http://60.204.154.12访问使用。项目以霍......
  • AiP74LVC1T45GB236.TR SOT23-6缓冲器/驱动器双电源接口电平转换
    AIP74LVC1T45GB236.TR是一款电平转换芯片,它的应用领域非常广泛,主要包括: 1.嵌入式系统:在嵌入式系统中,由于不同的外设可能工作在不同的电源电压下,该电平转换器可用于确保微控制器和其他逻辑电路之间的信号传输正确无误。 2.汽车电子:汽车电子系统经常需要处理来自不同电源......
  • 【Cesium】Vue+js+Cesium实现海康监控视频云台控制
    1.硬件设备与视频流接入    如需要一步上一篇博客【Cesium】Vue+js+Cesium实现监控视频流接入-CSDN博客文章浏览阅读308次,点赞12次,收藏17次。Vue2+js+Cesium实现监控视频流接入与相机云台控制https://blog.csdn.net/weixin_51540717/article/details/139614406?csdn_......
  • 一个完整回归机器学习案例
    如何端到端解决预测建模机器学习问题?我们将通过一个案例研究Python中的回归预测建模问题,包括应用机器学习过程的每一步。完成这个项目后,我们要知道:如何端到端解决回归预测建模问题如何使用数据转换来提高模型性能如何使用算法调优来提高模型性能如何使用集成方法和集......
  • 基础设施建设——全局异常请求处理
    基础设施建设——全局异常请求处理1.引言在大型微服务架构中,伴随着错综复杂的调用链,统一的、全局的异常请求兜底处理就显得非常重要,如果没有全局统一的请求/响应规范,上下游之间的接口调用、协同配合将会变得异常困难,但是单纯的在业务逻辑中声明可能抛出的异常或者可能返回的错误......
  • 目标检测中的anchor机制
    目录一、目标检测中的anchor机制1.什么是anchorboxes?二、什么是Anchor?​编辑三、为什么需要anchorboxes?四、anchorboxes是怎么生成的?五、高宽比(aspectratio)的确定六、尺度(scale)的确定七、anchorboxes数量的确定八、Anchorboxes的用途  九、anchorboxes对......
  • 自动求导
    向量链式法则标量链式法则拓展到向量例1例2自动求导自动求导计算一个函数在指定值上的导数计算图将代码分解成操作子将计算表示成一个无环图显式构造(Tensorflow/Theano/MXNet)隐式构造(Pytorch/MXNet)自动求导的两种模式正向累积反向累积(反向传递)a.构造计......