Implementing Nearby Search and Tinder-like Features

Location Reporting

When the client detects a user's geographic location, it reports to the server if the locasion changes by more than 500 meters or every 5 minutes. User locasion data is stored in Elasticsearch.

Dubbo Service

User location functionality is implemented in a new project called my-tanhua-dubbo-es.

POM Configuration

<dependencies>
    <dependency>
        <groupId>cn.itcast.tanhua</groupId>
        <artifactId>my-tanhua-dubbo-interface</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.boot</groupId>
        <artifactId>dubbo-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>dubbo</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.sgroschupf</groupId>
        <artifactId>zkclient</artifactId>
    </dependency>
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
    </dependency>
</dependencies>

Application Properties

spring.application.name=itcast-tanhua-dubbo-es
dubbo.scan.basePackages=com.tanhua.dubbo.es
dubbo.application.name=dubbo-provider-es
dubbo.protocol.name=dubbo
dubbo.protocol.port=20882
dubbo.registry.address=zookeeper://192.168.31.81:2181
dubbo.registry.client=zkclient
dubbo.registry.timeout=60000
spring.data.elasticsearch.cluster-name=es-tanhua-cluster
spring.data.elasticsearch.cluster-nodes=192.168.31.81:9300,192.168.31.81:9301,192.168.31.81:9302

Startup Class

@SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class})
public class ESApplication {
    public static void main(String[] args) {
        SpringApplication.run(ESApplication.class, args);
    }
}

Data Models

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "tanhua", type = "user_location", shards = 6, replicas = 2)
public class UserLocation {
    @Id
    private Long userId;
    @GeoPointField
    private GeoPoint location;
    @Field(type = FieldType.Keyword)
    private String address;
    @Field(type = FieldType.Long)
    private Long created;
    @Field(type = FieldType.Long)
    private Long updated;
    @Field(type = FieldType.Long)
    private Long lastUpdated;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserLocationVo implements Serializable {
    private static final long serialVersionUID = 4133419501260037769L;
    private Long userId;
    private Double longitude;
    private Double latitude;
    private String address;
    private Long created;
    private Long updated;
    private Long lastUpdated;

    public static UserLocationVo format(UserLocation location) {
        UserLocationVo vo = BeanUtil.toBean(location, UserLocationVo.class);
        vo.setLongitude(location.getLocation().getLon());
        vo.setLatitude(location.getLocation().getLat());
        return vo;
    }

    public static List<userlocationvo> formatToList(List<UserLocation> locations) {
        List<UserLocationVo> list = new ArrayList<>();
        for (UserLocation location : locations) {
            list.add(format(location));
        }
        return list;
    }
}</userlocationvo>

Dubbo Interface

public interface UserLocationApi {
    Boolean updateUserLocation(Long userId, Double longitude, Double latitude, String address);
}

Service Implementation

@Service(version = "1.0.0")
@Slf4j
public class UserLocationApiImpl implements UserLocationApi {
    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;

    @PostConstruct
    public void initIndex() {
        if (!elasticsearchTemplate.indexExists("tanhua")) {
            elasticsearchTemplate.createIndex(UserLocation.class);
        }
        if (!elasticsearchTemplate.typeExists("tanhua", "user_location")) {
            elasticsearchTemplate.putMapping(UserLocation.class);
        }
    }

    @Override
    public Boolean updateUserLocation(Long userId, Double longitude, Double latitude, String address) {
        try {
            GetQuery getQuery = new GetQuery();
            getQuery.setId(String.valueOf(userId));
            UserLocation userLocation = elasticsearchTemplate.queryForObject(getQuery, UserLocation.class);

            if (ObjectUtil.isEmpty(userLocation)) {
                userLocation = new UserLocation();
                userLocation.setUserId(userId);
                userLocation.setAddress(address);
                userLocation.setCreated(System.currentTimeMillis());
                userLocation.setUpdated(userLocation.getCreated());
                userLocation.setLastUpdated(userLocation.getCreated());
                userLocation.setLocation(new GeoPoint(latitude, longitude));

                IndexQuery indexQuery = new IndexQueryBuilder().withObject(userLocation).build();
                elasticsearchTemplate.index(indexQuery);
            } else {
                Map<String, Object> updateData = new HashMap<>();
                updateData.put("location", new GeoPoint(latitude, longitude));
                updateData.put("updated", System.currentTimeMillis());
                updateData.put("lastUpdated", userLocation.getUpdated());
                updateData.put("address", address);

                UpdateRequest updateRequest = new UpdateRequest();
                updateRequest.doc(updateData);

                UpdateQuery updateQuery = new UpdateQueryBuilder()
                        .withId(String.valueOf(userId))
                        .withClass(UserLocation.class)
                        .withUpdateRequest(updateRequest)
                        .build();

                elasticsearchTemplate.update(updateQuery);
            }

            return true;
        } catch (Exception e) {
            log.error("Failed to update location for user: {}", userId, e);
            return false;
        }
    }
}

Unit Test

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestUserLocationApi {
    @Autowired
    private UserLocationApi userLocationApi;

    @Test
    public void testUpdateUserLocation() {
        List<Object[]> testData = Arrays.asList(
            new Object[]{1L, 121.512253, 31.24094, "Jinmao Tower"},
            new Object[]{2L, 121.506377, 31.245105, "Oriental Pearl Tower"},
            new Object[]{10L, 121.508815, 31.243844, "Lujiazui Metro Station"}
        );

        for (Object[] data : testData) {
            userLocationApi.updateUserLocation((Long)data[0], (Double)data[1], (Double)data[2], (String)data[3]);
        }
    }
}

Nearby Search

Dubbo Service

Interface Methods

UserLocationVo queryByUserId(Long userId);
PageInfo<UserLocationVo> queryUserFromLocation(Double longitude, Double latitude, Double distance, Integer page, Integer pageSize);

Implementation

@Override
public UserLocationVo queryByUserId(Long userId) {
    GetQuery getQuery = new GetQuery();
    getQuery.setId(String.valueOf(userId));
    UserLocation location = elasticsearchTemplate.queryForObject(getQuery, UserLocation.class);
    return location != null ? UserLocationVo.format(location) : null;
}

@Override
public PageInfo<UserLocationVo> queryUserFromLocation(Double longitude, Double latitude, Double distance, Integer page, Integer pageSize) {
    PageInfo<UserLocationVo> pageInfo = new PageInfo<>();
    pageInfo.setPageNum(page);
    pageInfo.setPageSize(pageSize);

    NativeSearchQueryBuilder searchQueryBuilder = new NativeSearchQueryBuilder();
    PageRequest pageRequest = PageRequest.of(page - 1, pageSize);
    searchQueryBuilder.withPageable(pageRequest);

    BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
    GeoDistanceQueryBuilder geoDistanceQueryBuilder = new GeoDistanceQueryBuilder("location");
    geoDistanceQueryBuilder.point(new GeoPoint(latitude, longitude));
    geoDistanceQueryBuilder.distance(distance / 1000, DistanceUnit.KILOMETERS);
    boolQueryBuilder.must(geoDistanceQueryBuilder);
    searchQueryBuilder.withQuery(boolQueryBuilder);

    GeoDistanceSortBuilder geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", latitude, longitude);
    geoDistanceSortBuilder.order(SortOrder.ASC);
    geoDistanceSortBuilder.unit(DistanceUnit.KILOMETERS);
    searchQueryBuilder.withSort(geoDistanceSortBuilder);

    AggregatedPage<UserLocation> resultPage = elasticsearchTemplate.queryForPage(searchQueryBuilder.build(), UserLocation.class);
    if (CollUtil.isEmpty(resultPage.getContent())) {
        return pageInfo;
    }

    pageInfo.setRecords(UserLocationVo.formatToList(resultPage.getContent()));
    return pageInfo;
}

Unit Test

@Test
public void testQueryUserFromLocation() {
    UserLocationVo userLocation = userLocationApi.queryByUserId(1L);
    PageInfo<UserLocationVo> results = userLocationApi.queryUserFromLocation(
        userLocation.getLongitude(), userLocation.getLatitude(), 5000d, 1, 10);
    results.getRecords().forEach(System.out::println);
}

APP Interface

Data Model

@Data
@NoArgsConstructor
@AllArgsConstructor
public class NearUserVo {
    private Long userId;
    private String avatar;
    private String nickname;
}

Controller

@RestController
@RequestMapping("baidu")
public class BaiduController {
    @Autowired
    private BaiduService baiduService;

    @PostMapping("location")
    public ResponseEntity<Void> updateLocation(@RequestBody Map<String, Object> param) {
        try {
            Double longitude = Double.valueOf(param.get("longitude").toString());
            Double latitude = Double.valueOf(param.get("latitude").toString());
            String address = param.get("addrStr").toString();

            Boolean result = baiduService.updateLocation(longitude, latitude, address);
            return result ? ResponseEntity.ok(null) : ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        } catch (Exception e) {
            log.error("Location update failed", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
}

Service

@Service
@Slf4j
public class BaiduService {
    @Reference(version = "1.0.0")
    private UserLocationApi userLocationApi;

    public Boolean updateLocation(Double longitude, Double latitude, String address) {
        User currentUser = UserThreadLocal.get();
        try {
            return userLocationApi.updateUserLocation(currentUser.getId(), longitude, latitude, address);
        } catch (Exception e) {
            log.error("Failed to update location for user: {}", currentUser.getId(), e);
            return false;
        }
    }
}

Tinder-like Feature

Like Service

Data Model

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "user_like")
public class UserLike implements Serializable {
    private static final long serialVersionUID = 6739966698394686523L;
    private ObjectId id;
    @Indexed
    private Long userId;
    @Indexed
    private Long likeUserId;
    private Long created;
}

Interface

public interface UserLikeApi {
    Boolean likeUser(Long userId, Long likeUserId);
    Boolean notLikeUser(Long userId, Long likeUserId);
    Boolean isMutualLike(Long userId, Long likeUserId);
    List<Long> queryLikeList(Long userId);
    List<Long> queryNotLikeList(Long userId);
}

Implementation

@Service(version = "1.0.0")
public class UserLikeApiImpl implements UserLikeApi {
    @Autowired
    private MongoTemplate mongoTemplate;
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public static final String LIKE_REDIS_KEY_PREFIX = "USER_LIKE_";
    public static final String NOT_LIKE_REDIS_KEY_PREFIX = "USER_NOT_LIKE_";

    @Override
    public Boolean likeUser(Long userId, Long likeUserId) {
        if (isLike(userId, likeUserId)) return false;

        UserLike userLike = new UserLike();
        userLike.setId(ObjectId.get());
        userLike.setUserId(userId);
        userLike.setLikeUserId(likeUserId);
        userLike.setCreated(System.currentTimeMillis());
        mongoTemplate.save(userLike);

        String redisKey = getLikeRedisKey(userId);
        String hashKey = String.valueOf(likeUserId);
        redisTemplate.opsForHash().put(redisKey, hashKey, "1");

        if (isNotLike(userId, likeUserId)) {
            redisTemplate.opsForHash().delete(getNotLikeRedisKey(userId), hashKey);
        }

        return true;
    }

    @Override
    public Boolean notLikeUser(Long userId, Long likeUserId) {
        if (isNotLike(userId, likeUserId)) return false;

        String redisKey = getNotLikeRedisKey(userId);
        String hashKey = String.valueOf(likeUserId);
        redisTemplate.opsForHash().put(redisKey, hashKey, "1");

        if (isLike(userId, likeUserId)) {
            Query query = Query.query(Criteria.where("userId").is(userId).and("likeUserId").is(likeUserId));
            mongoTemplate.remove(query, UserLike.class);
            redisTemplate.opsForHash().delete(getLikeRedisKey(userId), hashKey);
        }

        return true;
    }

    private Boolean isLike(Long userId, Long likeUserId) {
        return redisTemplate.opsForHash().hasKey(getLikeRedisKey(userId), String.valueOf(likeUserId));
    }

    private Boolean isNotLike(Long userId, Long likeUserId) {
        return redisTemplate.opsForHash().hasKey(getNotLikeRedisKey(userId), String.valueOf(likeUserId));
    }

    @Override
    public Boolean isMutualLike(Long userId, Long likeUserId) {
        return isLike(userId, likeUserId) && isLike(likeUserId, userId);
    }

    @Override
    public List<Long> queryLikeList(Long userId) {
        Set<Object> keys = redisTemplate.opsForHash().keys(getLikeRedisKey(userId));
        if (CollUtil.isEmpty(keys)) return ListUtil.empty();

        List<Long> result = new ArrayList<>(keys.size());
        keys.forEach(o -> result.add(Convert.toLong(o)));
        return result;
    }

    @Override
    public List<Long> queryNotLikeList(Long userId) {
        Set<Object> keys = redisTemplate.opsForHash().keys(getNotLikeRedisKey(userId));
        if (CollUtil.isEmpty(keys)) return ListUtil.empty();

        List<Long> result = new ArrayList<>(keys.size());
        keys.forEach(o -> result.add(Convert.toLong(o)));
        return result;
    }

    private String getLikeRedisKey(Long userId) {
        return LIKE_REDIS_KEY_PREFIX + userId;
    }

    private String getNotLikeRedisKey(Long userId) {
        return NOT_LIKE_REDIS_KEY_PREFIX + userId;
    }
}

Unit Test

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestUserLikeApi {
    @Autowired
    private UserLikeApi userLikeApi;

    @Test
    public void testUserLike() {
        List<Object[]> testData = Arrays.asList(
            new Object[]{1L, 2L},
            new Object[]{1L, 3L},
            new Object[]{1L, 4L}
        );

        for (Object[] data : testData) {
            userLikeApi.likeUser((Long)data[0], (Long)data[1]);
        }
    }

    @Test
    public void testQueryList() {
        userLikeApi.queryLikeList(1L).forEach(System.out::println);
        System.out.println("-------");
        userLikeApi.queryNotLikeList(1L).forEach(System.out::println);
    }
}

Recommendation Service

Interface

List<RecommendUser> queryCardList(Long userId, Integer count);

Implementation

@Override
public List<RecommendUser> queryCardList(Long userId, Integer count) {
    PageRequest pageRequest = PageRequest.of(0, count, Sort.by(Sort.Order.desc("score")));

    List<Long> excludedIds = new ArrayList<>();
    excludedIds.addAll(userLikeApi.queryLikeList(userId));
    excludedIds.addAll(userLikeApi.queryNotLikeList(userId));

    Criteria criteria = Criteria.where("toUserId").is(userId);
    if (!excludedIds.isEmpty()) {
        criteria.andOperator(Criteria.where("userId").nin(excludedIds));
    }

    Query query = Query.query(criteria).with(pageRequest);
    return mongoTemplate.find(query, RecommendUser.class);
}

Unit Test

@Test
public void testQueryCardList() {
    userLikeApi.queryCardList(2L, 20).forEach(System.out::println);
}

Profile Management

Basic Information

@PutMapping
public ResponseEntity<Void> updateUserInfo(@RequestBody UserInfoVo userInfo) {
    try {
        Boolean result = myCenterService.updateUserInfo(userInfo);
        return result ? ResponseEntity.ok(null) : ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    } catch (Exception e) {
        log.error("Failed to update user info", e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
}
public Boolean updateUserInfo(UserInfoVo userInfoVo) {
    User currentUser = UserThreadLocal.get();
    UserInfo updatedInfo = new UserInfo();
    updatedInfo.setUserId(currentUser.getId());
    updatedInfo.setAge(Integer.valueOf(userInfoVo.getAge()));
    updatedInfo.setSex(StringUtils.equalsIgnoreCase(userInfoVo.getGender(), "man") ? SexEnum.MAN : SexEnum.WOMAN);
    updatedInfo.setBirthday(userInfoVo.getBirthday());
    updatedInfo.setCity(userInfoVo.getCity());
    updatedInfo.setEdu(userInfoVo.getEducation());
    updatedInfo.setIncome(StringUtils.replaceAll(userInfoVo.getIncome(), "K", ""));
    updatedInfo.setIndustry(userInfoVo.getProfession());
    updatedInfo.setMarriage(userInfoVo.getMarriage() == 1 ? "已婚" : "未婚");
    return userInfoService.updateUserInfoByUserId(updatedInfo);
}
public boolean updateUserInfoByUserId(UserInfo info) {
    QueryWrapper<UserInfo> query = new QueryWrapper<>();
    query.eq("user_id", info.getUserId());
    return userInfoMapper.update(info, query) > 0;
}

Avatar Upload

location /users/header {
    client_max_body_size 300m;
    proxy_connect_timeout 300s;
    proxy_send_timeout 300s;
    proxy_read_timeout 300s;
    proxy_pass http://127.0.0.1:18080;
}
@RestController
@RequestMapping("users")
public class MyCenterController {
    @Autowired
    private UserInfoController userInfoController;

    @PostMapping("header")
    public ResponseEntity<Object> saveLogo(@RequestParam("headPhoto") MultipartFile file, @RequestHeader("Authorization") String token) {
        return userInfoController.saveUserLogo(file, token);
    }
}

Tags: elasticsearch Redis mongodb SpringBoot Dubbo

Posted on Sat, 16 May 2026 10:42:54 +0000 by TheUkSniper