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