天天看點

七(7)探花功能-MongoDB地理位置查詢-附近的人

課程總結

1.探花功能

  1. 業務需求
  2. 執行過程

2.MongoDB的地理位置查詢

  1. 地理位置查詢的應用場景
  2. 查詢案例

3.搜附近

  1. 上報地理位置
  2. 使用MongoDB搜尋附近

一. 探花左劃右滑

探花功能是将推薦的好友随機的通過卡片的形式展現出來,使用者可以選擇左滑、右滑操作,左滑:“不喜歡”,右滑:“喜歡”。

喜歡:如果雙方喜歡,那麼就會成為好友。

七(7)探花功能-MongoDB地理位置查詢-附近的人

如果已經喜歡或不喜歡的使用者在清單中不再顯示。

1-1 技術支援

1. 資料庫表

七(7)探花功能-MongoDB地理位置查詢-附近的人

2. Redis緩存

探花功能使用Redis緩存提高查詢效率。

對于喜歡/不喜歡功能,使用Redis中的set進行存儲。Redis 的 Set 是無序集合。集合成員是唯一的,這就意味着集合中不能出現重複的資料。使用Set可以友善的實作交集,并集等個性化查詢。

資料規則:

喜歡:USER_LIKE_SET_{ID}

不喜歡:USER_NOT_LIKE_SET_{ID}

1-2 查詢推薦使用者

1. 接口文檔

七(7)探花功能-MongoDB地理位置查詢-附近的人

單次查詢随機傳回10條推薦使用者資料

需要排除已喜歡/不喜歡的資料

2. 思路分析

查詢探花卡片推薦使用者清單

1、查詢已喜歡/不喜歡的使用者清單

2、查詢推薦清單,并排除已喜歡/不喜歡使用者

3、如果推薦清單不存在,構造預設資料

七(7)探花功能-MongoDB地理位置查詢-附近的人

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. 接口文檔

喜歡接口

七(7)探花功能-MongoDB地理位置查詢-附近的人

不喜歡接口

七(7)探花功能-MongoDB地理位置查詢-附近的人

2. 思路分析

喜歡思路分析

七(7)探花功能-MongoDB地理位置查詢-附近的人

喜歡步驟

1、編寫API層方法,将喜歡資料存入MongoDB

查詢是否存在喜歡資料

如果存在更新資料,如果不存在儲存資料

2、Service層進行代碼調用

調用API層儲存喜歡資料,喜歡資料寫入Redis

判斷兩者是否互相喜歡

如果互相喜歡,完成好友添加

3、Redis中的操作

七(7)探花功能-MongoDB地理位置查詢-附近的人

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

    ,兩者都包括在内。
七(7)探花功能-MongoDB地理位置查詢-附近的人

LineString

Polygon

{
  type: "Polygon",
  coordinates: [ [ [ 0 , 0 ] , [ 3 , 6 ] , [ 6 , 1 ] , [ 0 , 0  ] ] ]
}
           

2-2 案例

查詢附近并按照距離傳回

1. 環境準備

1、建立資料庫

2、建立索引

3、導入資料

4、配置實體類

七(7)探花功能-MongoDB地理位置查詢-附近的人
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方法是根據間距排序的

總結

  1. MongoDB地理位置配置索引,推薦使用2dsphere
  2. 實體類使用GeoJsonPoint指定地理位置(經緯度)
  3. 查詢附近使用MongoTemplate的withinSphere方法
  4. 查詢并擷取距離使用geoNear方法

三. 附近的人

3-1 上報地理位置

當用戶端檢測使用者的地理位置,當變化大于500米時或每隔5分鐘,向服務端上報地理位置。

使用者的地理位置存儲到MongoDB中,如下:

七(7)探花功能-MongoDB地理位置查詢-附近的人

1. 執行流程

七(7)探花功能-MongoDB地理位置查詢-附近的人

2. 表結構

七(7)探花功能-MongoDB地理位置查詢-附近的人

user_location表中已經指定location字段索引類型: 2DSphere

對于同一個使用者,隻有一個地理位置資料

3. 思路分析

思考:如何擷取并上報地理位置?

  1. 用戶端定位擷取地理位置資訊
  2. 用戶端定時發送定位資料(5分鐘)
  3. 用戶端檢測移動距離發送定位資料(大于500米)

需求:實作上報地理位置功能(首次上報儲存資料,後續上報更新資料)

注意事項: GeoJsonPoint對象不支援序列化 ( 在消費者和提供者直接不能傳遞GeoJsonPoint對象 )

代碼步驟:

  1. 搭建提供者環境
  2. 編寫Controller接受請求參數
  3. 編寫Service調用API完成上報地理位置功能
  4. 在API層完成更新或者儲存操作

4. 接口文檔

七(7)探花功能-MongoDB地理位置查詢-附近的人

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 搜附近

在首頁中點選“搜附近”可以搜尋附近的好友,效果如下:

七(7)探花功能-MongoDB地理位置查詢-附近的人

實作思路:根據目前使用者的位置,查詢附近範圍内的使用者。範圍是可以設定的。

1. 接口文檔

七(7)探花功能-MongoDB地理位置查詢-附近的人

2. 實作步驟

七(7)探花功能-MongoDB地理位置查詢-附近的人

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);
    }