課程總結
1.探花功能
- 業務需求
- 執行過程
2.MongoDB的地理位置查詢
- 地理位置查詢的應用場景
- 查詢案例
3.搜附近
- 上報地理位置
- 使用MongoDB搜尋附近
一. 探花左劃右滑
探花功能是将推薦的好友随機的通過卡片的形式展現出來,使用者可以選擇左滑、右滑操作,左滑:“不喜歡”,右滑:“喜歡”。
喜歡:如果雙方喜歡,那麼就會成為好友。
如果已經喜歡或不喜歡的使用者在清單中不再顯示。
1-1 技術支援
1. 資料庫表
2. Redis緩存
探花功能使用Redis緩存提高查詢效率。
對于喜歡/不喜歡功能,使用Redis中的set進行存儲。Redis 的 Set 是無序集合。集合成員是唯一的,這就意味着集合中不能出現重複的資料。使用Set可以友善的實作交集,并集等個性化查詢。
資料規則:
喜歡:USER_LIKE_SET_{ID}
不喜歡:USER_NOT_LIKE_SET_{ID}
1-2 查詢推薦使用者
1. 接口文檔
單次查詢随機傳回10條推薦使用者資料
需要排除已喜歡/不喜歡的資料
2. 思路分析
查詢探花卡片推薦使用者清單
1、查詢已喜歡/不喜歡的使用者清單
2、查詢推薦清單,并排除已喜歡/不喜歡使用者
3、如果推薦清單不存在,構造預設資料
3. 實體對象
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "user_like")
public class UserLike implements java.io.Serializable {
private static final long serialVersionUID = 6739966698394686523L;
private ObjectId id;
@Indexed
private Long userId; //使用者id,自己
@Indexed
private Long likeUserId; //喜歡的使用者id,對方
private Boolean isLike; // 是否喜歡
private Long created; //建立時間
private Long updated; // 更新時間
}
4. controller
TanhuaController
/**
* 探花-推薦使用者清單
*/
@GetMapping("/cards")
public ResponseEntity queryCardsList() {
List<TodayBest> list = this.tanhuaService.queryCardsList();
return ResponseEntity.ok(list);
}
5. service
TanhuaService
#預設推薦清單
tanhua:
default:
recommend:
users: 2,3,8,10,18,20,24,29,27,32,36,37,56,64,75,88
@Value("${tanhua.default.recommend.users}")
private String recommendUser;
//探花-推薦使用者清單
public List<TodayBest> queryCardsList() {
//1、調用推薦API查詢資料清單(排除喜歡/不喜歡的使用者,數量限制)
List<RecommendUser> users = recommendUserApi.queryCardsList(UserHolder.getUserId(),10);
//2、判斷資料是否存在,如果不存在,構造預設資料 1,2,3
if(CollUtil.isEmpty(users)) {
users = new ArrayList<>();
String[] userIdS = recommendUser.split(",");
for (String userId : userIdS) {
RecommendUser recommendUser = new RecommendUser();
recommendUser.setUserId(Convert.toLong(userId));
recommendUser.setToUserId(UserHolder.getUserId());
recommendUser.setScore(RandomUtil.randomDouble(60, 90));
users.add(recommendUser);
}
}
//3、構造VO
List<Long> ids = CollUtil.getFieldValues(users, "userId", Long.class);
Map<Long, UserInfo> infoMap = userInfoApi.findByIds(ids, null);
List<TodayBest> vos = new ArrayList<>();
for (RecommendUser user : users) {
UserInfo userInfo = infoMap.get(user.getUserId());
if(userInfo != null) {
TodayBest vo = TodayBest.init(userInfo, user);
vos.add(vo);
}
}
return vos;
}
6. API實作
RecommendUserApi
/**
* 查詢探花清單,查詢時需要排除喜歡和不喜歡的使用者
*/
List<RecommendUser> queryCardsList(Long userId, int count);
RecommendUserApiImpl
/**
* 查詢探花清單,查詢時需要排除喜歡和不喜歡的使用者
* 1、排除喜歡,不喜歡的使用者
* 2、随機展示
* 3、指定數量
*/
public List<RecommendUser> queryCardsList(Long userId, int counts) {
//1、查詢喜歡不喜歡的使用者ID
List<UserLike> likeList = mongoTemplate.find(Query.query(Criteria.where("userId").is(userId)), UserLike.class);
List<Long> likeUserIdS = CollUtil.getFieldValues(likeList, "likeUserId", Long.class);
//2、構造查詢推薦使用者的條件
Criteria criteria = Criteria.where("toUserId").is(userId).and("userId").nin(likeUserIdS);
//3、使用統計函數,随機擷取推薦的使用者清單
TypedAggregation<RecommendUser> newAggregation = TypedAggregation.newAggregation(RecommendUser.class,
Aggregation.match(criteria),//指定查詢條件
Aggregation.sample(counts)
);
AggregationResults<RecommendUser> results = mongoTemplate.aggregate(newAggregation, RecommendUser.class);
//4、構造傳回
return results.getMappedResults();
}
1-3 喜歡&不喜歡(Redis-set)
使用者的喜歡與不喜歡清單需要儲存在redis中,為了防止redis中的資料丢失,同時需要将資料儲存到mongodb進行持久化儲存。
左滑:“不喜歡”,右滑:“喜歡”,如果雙方喜歡,那麼就會成為好友。
1. 接口文檔
喜歡接口
不喜歡接口
2. 思路分析
喜歡思路分析
喜歡步驟
1、編寫API層方法,将喜歡資料存入MongoDB
查詢是否存在喜歡資料
如果存在更新資料,如果不存在儲存資料
2、Service層進行代碼調用
調用API層儲存喜歡資料,喜歡資料寫入Redis
判斷兩者是否互相喜歡
如果互相喜歡,完成好友添加
3、Redis中的操作
3. TanHuaController
/**
* 喜歡
*/
@GetMapping("{id}/love")
public ResponseEntity<Void> likeUser(@PathVariable("id") Long likeUserId) {
this.tanhuaService.likeUser(likeUserId);
return ResponseEntity.ok(null);
}
/**
* 不喜歡
*/
@GetMapping("{id}/unlove")
public ResponseEntity<Void> notLikeUser(@PathVariable("id") Long likeUserId) {
this.tanhuaService.notLikeUser(likeUserId);
return ResponseEntity.ok(null);
}
4. TanHuaService
@Autowired
private MessagesService messagesService;
//探花喜歡 106 - 2
public void likeUser(Long likeUserId) {
//1、調用API,儲存喜歡資料(儲存到MongoDB中)
Boolean save = userLikeApi.saveOrUpdate(UserHolder.getUserId(),likeUserId,true);
if(!save) {
//失敗
throw new BusinessException(ErrorResult.error());
}
//2、操作redis,寫入喜歡的資料,删除不喜歡的資料 (喜歡的集合,不喜歡的集合)
redisTemplate.opsForSet().remove(Constants.USER_NOT_LIKE_KEY+UserHolder.getUserId(),likeUserId.toString());
redisTemplate.opsForSet().add(Constants.USER_LIKE_KEY+UserHolder.getUserId(),likeUserId.toString());
//3、判斷是否雙向喜歡
if(isLike(likeUserId,UserHolder.getUserId())) {
//4、添加好友
messagesService.contacts(likeUserId);
}
}
public Boolean isLike(Long userId,Long likeUserId) {
String key = Constants.USER_LIKE_KEY+userId;
return redisTemplate.opsForSet().isMember(key,likeUserId.toString());
}
//不喜歡
public void notLikeUser(Long likeUserId) {
//1、調用API,儲存喜歡資料(儲存到MongoDB中)
Boolean save = userLikeApi.saveOrUpdate(UserHolder.getUserId(),likeUserId,false);
if(!save) {
//失敗
throw new BusinessException(ErrorResult.error());
}
//2、操作redis,寫入喜歡的資料,删除不喜歡的資料 (喜歡的集合,不喜歡的集合)
redisTemplate.opsForSet().add(Constants.USER_NOT_LIKE_KEY+UserHolder.getUserId(),likeUserId.toString());
redisTemplate.opsForSet().remove(Constants.USER_LIKE_KEY+UserHolder.getUserId(),likeUserId.toString());
//3、判斷是否雙向喜歡,删除好友(各位自行實作)
}
5. API和實作類
public interface UserLikeApi {
//儲存或者更新
Boolean saveOrUpdate(Long userId, Long likeUserId, boolean isLike);
}
@DubboService
public class UserLikeApiImpl implements UserLikeApi{
@Autowired
private MongoTemplate mongoTemplate;
@Override
public Boolean saveOrUpdate(Long userId, Long likeUserId, boolean isLike) {
try {
//1、查詢資料
Query query = Query.query(Criteria.where("userId").is(userId).and("likeUserId").is(likeUserId));
UserLike userLike = mongoTemplate.findOne(query, UserLike.class);
//2、如果不存在,儲存
if(userLike == null) {
userLike = new UserLike();
userLike.setUserId(userId);
userLike.setLikeUserId(likeUserId);
userLike.setCreated(System.currentTimeMillis());
userLike.setUpdated(System.currentTimeMillis());
userLike.setIsLike(isLike);
mongoTemplate.save(userLike);
}else {
//3、更新
Update update = Update.update("isLike", isLike)
.set("updated",System.currentTimeMillis());
mongoTemplate.updateFirst(query,update,UserLike.class);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
二. MongoDB地理位置檢索
2-1 地理位置索引
随着網際網路5G網絡的發展, 定位技術越來越精确,地理位置的服務(Location Based Services,LBS)已經滲透到各個軟體應用中。如網約車平台,外賣,社交軟體,物流等。
目前很多資料庫都支援地理位置檢索服務,如:MySQL,MongoDB, Elasticsearch等。我們的課程使用MongoDB實作。MongoDB 支援對地理空間資料的查詢操作。
在MongoDB中使用地理位置查詢,必須建立索引才能使用,其内部支援兩種索引類型
- 2d
支援二維平面的地理位置資料運算 , 能夠将資料作為二維平面上的點存儲起來
- 2dsphere
支援類地球的球面上進行幾何計算 , 以GeoJSON對象或者普通坐标對的方式存儲資料。
MongoDB内部支援多種GeoJson對象類型:
Point
最基礎的坐标點,指定緯度和經度坐标,首先列出經度,然後列出 緯度:
- 有效的經度值介于
和之間-180
,兩者都包括在内。180
- 有效的緯度值介于
和之間-90
,兩者都包括在内。90
LineString
Polygon
{
type: "Polygon",
coordinates: [ [ [ 0 , 0 ] , [ 3 , 6 ] , [ 6 , 1 ] , [ 0 , 0 ] ] ]
}
2-2 案例
查詢附近并按照距離傳回
1. 環境準備
1、建立資料庫
2、建立索引
3、導入資料
4、配置實體類
GeoJsonPoint : 地理位置坐标(經緯度)
2. 查詢附近
查詢目前坐标附近的目标
@Test
public void testNear() {
//構造坐标點 (經度 - 緯度)
GeoJsonPoint point = new GeoJsonPoint(116.404, 39.915);
//構造半徑 (距離 - 距離機關)
Distance distanceObj = new Distance(1, Metrics.KILOMETERS);
//畫了一個圓圈(圓點)
Circle circle = new Circle(point, distanceObj);
//構造query對象
Query query = Query.query(Criteria.where("location").withinSphere(circle));
//查詢
List<Places> list = mongoTemplate.find(query, Places.class);
list.forEach(System.out::println);
}
3. 查詢并擷取距離
我們假設需要以目前坐标為原點,查詢附近指定範圍内的餐廳,并直接顯示距離
//查詢附近且擷取間距
@Test
public void testNear1() {
//1、構造中心點(圓點)
GeoJsonPoint point = new GeoJsonPoint(116.404, 39.915);
//2、建構NearQuery對象
NearQuery query = NearQuery.near(point, Metrics.KILOMETERS).maxDistance(1, Metrics.KILOMETERS);
//3、調用mongoTemplate的geoNear方法查詢
GeoResults<Places> results = mongoTemplate.geoNear(query, Places.class);
//4、解析GeoResult對象,擷取距離和資料
for (GeoResult<Places> result : results) {
Places places = result.getContent();
double value = result.getDistance().getValue();
System.out.println(places+"---距離:"+value + "km");
}
}
geoNear方法是根據間距排序的
總結
- MongoDB地理位置配置索引,推薦使用2dsphere
- 實體類使用GeoJsonPoint指定地理位置(經緯度)
- 查詢附近使用MongoTemplate的withinSphere方法
- 查詢并擷取距離使用geoNear方法
三. 附近的人
3-1 上報地理位置
當用戶端檢測使用者的地理位置,當變化大于500米時或每隔5分鐘,向服務端上報地理位置。
使用者的地理位置存儲到MongoDB中,如下:
1. 執行流程
2. 表結構
user_location表中已經指定location字段索引類型: 2DSphere
對于同一個使用者,隻有一個地理位置資料
3. 思路分析
思考:如何擷取并上報地理位置?
- 用戶端定位擷取地理位置資訊
- 用戶端定時發送定位資料(5分鐘)
- 用戶端檢測移動距離發送定位資料(大于500米)
需求:實作上報地理位置功能(首次上報儲存資料,後續上報更新資料)
注意事項: GeoJsonPoint對象不支援序列化 ( 在消費者和提供者直接不能傳遞GeoJsonPoint對象 )
代碼步驟:
- 搭建提供者環境
- 編寫Controller接受請求參數
- 編寫Service調用API完成上報地理位置功能
- 在API層完成更新或者儲存操作
4. 接口文檔
5. 定義pojo
在my-tanhua-dubbo-interface中建立:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "user_location")
@CompoundIndex(name = "location_index", def = "{'location': '2dsphere'}")
public class UserLocation implements java.io.Serializable{
private static final long serialVersionUID = 4508868382007529970L;
@Id
private ObjectId id;
@Indexed
private Long userId; //使用者id
private GeoJsonPoint location; //x:經度 y:緯度
private String address; //位置描述
private Long created; //建立時間
private Long updated; //更新時間
private Long lastUpdated; //上次更新時間
}
6. BaiduController
@RestController
@RequestMapping("/baidu")
public class BaiduController {
@Autowired
private BaiduService baiduService;
/**
* 更新位置
*/
@PostMapping("/location")
public ResponseEntity updateLocation(@RequestBody Map param) {
Double longitude = Double.valueOf(param.get("longitude").toString());
Double latitude = Double.valueOf(param.get("latitude").toString());
String address = param.get("addrStr").toString();
this.baiduService.updateLocation(longitude, latitude,address);
return ResponseEntity.ok(null);
}
}
7. BaiduService
@Service
public class BaiduService {
@DubboReference
private UserLocationApi userLocationApi;
//更新地理位置
public void updateLocation(Double longitude, Double latitude, String address) {
Boolean flag = userLocationApi.updateLocation(UserHolder.getUserId(),longitude,latitude,address);
if(!flag) {
throw new BusinessException(ErrorResult.error());
}
}
}
8. API實作
在my-tanhua-dubbo-interface工程中。
public interface UserLocationApi {
//更新地理位置
Boolean updateLocation(Long userId, Double longitude, Double latitude, String address);
}
UserLocationApiImpl
@DubboService
public class UserLocationApiImpl implements UserLocationApi{
@Autowired
private MongoTemplate mongoTemplate;
//更新地理位置
public Boolean updateLocation(Long userId, Double longitude, Double latitude, String address) {
try {
//1、根據使用者id查詢位置資訊
Query query = Query.query(Criteria.where("userId").is(userId));
UserLocation location = mongoTemplate.findOne(query, UserLocation.class);
if(location == null) {
//2、如果不存在使用者位置資訊,儲存
location = new UserLocation();
location.setUserId(userId);
location.setAddress(address);
location.setCreated(System.currentTimeMillis());
location.setUpdated(System.currentTimeMillis());
location.setLastUpdated(System.currentTimeMillis());
location.setLocation(new GeoJsonPoint(longitude,latitude));
mongoTemplate.save(location);
}else {
//3、如果存在,更新
Update update = Update.update("location", new GeoJsonPoint(longitude, latitude))
.set("updated", System.currentTimeMillis())
.set("lastUpdated", location.getUpdated());
mongoTemplate.updateFirst(query,update,UserLocation.class);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
3-2 搜附近
在首頁中點選“搜附近”可以搜尋附近的好友,效果如下:
實作思路:根據目前使用者的位置,查詢附近範圍内的使用者。範圍是可以設定的。
1. 接口文檔
2. 實作步驟
3. NearUserVo
//附近的人vo對象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NearUserVo {
private Long userId;
private String avatar;
private String nickname;
public static NearUserVo init(UserInfo userInfo) {
NearUserVo vo = new NearUserVo();
vo.setUserId(userInfo.getId());
vo.setAvatar(userInfo.getAvatar());
vo.setNickname(userInfo.getNickname());
return vo;
}
}
4. TanHuaController
/**
* 搜附近
*/
@GetMapping("/search")
public ResponseEntity<List<NearUserVo>> queryNearUser(String gender,
@RequestParam(defaultValue = "2000") String distance) {
List<NearUserVo> list = this.tanhuaService.queryNearUser(gender, distance);
return ResponseEntity.ok(list);
}
5. TanHuaService
//搜附近
public List<NearUserVo> queryNearUser(String gender, String distance) {
//1、調用API查詢附近的使用者(傳回的是附近的人的所有使用者id,包含目前使用者的id)
List<Long> userIds = userLocationApi.queryNearUser(UserHolder.getUserId(),Double.valueOf(distance));
//2、判斷集合是否為空
if(CollUtil.isEmpty(userIds)) {
return new ArrayList<>();
}
//3、調用UserInfoApi根據使用者id查詢使用者詳情
UserInfo userInfo = new UserInfo();
userInfo.setGender(gender);
Map<Long, UserInfo> map = userInfoApi.findByIds(userIds, userInfo);
//4、構造傳回值。
List<NearUserVo> vos = new ArrayList<>();
for (Long userId : userIds) {
//排除目前使用者
if(userId == UserHolder.getUserId()) {
continue;
}
UserInfo info = map.get(userId);
if(info != null) {
NearUserVo vo = NearUserVo.init(info);
vos.add(vo);
}
}
return vos;
}
6. API實作
UserLocationApi
/**
* 根據位置搜尋附近人的所有ID
*/
List<Long> queryNearUser(Long userId, Double metre);
UserLocationApiImpl
@Override
public List<Long> queryNearUser(Long userId, Double metre) {
//1、根據使用者id,查詢使用者的位置資訊
Query query = Query.query(Criteria.where("userId").is(userId));
UserLocation location = mongoTemplate.findOne(query, UserLocation.class);
if(location == null) {
return null;
}
//2、已目前使用者位置繪制原點
GeoJsonPoint point = location.getLocation();
//3、繪制半徑
Distance distance = new Distance(metre / 1000, Metrics.KILOMETERS);
//4、繪制圓形
Circle circle = new Circle(point, distance);
//5、查詢
Query locationQuery = Query.query(Criteria.where("location").withinSphere(circle));
List<UserLocation> list = mongoTemplate.find(locationQuery, UserLocation.class);
return CollUtil.getFieldValues(list,"userId",Long.class);
}