08-搜附近以及探花功能實現


課程說明

  • 上報地位位置
  • 實現搜附近功能
  • 實現探花功能
  • 用戶基本信息維護

1、上報地理位置

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

用戶的地理位置存儲到Elasticsearch中,需要使用環境提供的ES集群,如下:

image-20210108104814073

1.1、dubbo服務

用戶地理位置的服務獨立一個新的工程來實現,名字為:my-tanhua-dubbo-es。

1.1.1、創建工程

pom.ml文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>my-tanhua-dubbo</artifactId>
        <groupId>cn.itcast.tanhua</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>my-tanhua-dubbo-es</artifactId>

    <dependencies>
        <!--引入interface依賴-->
        <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>
        <!--dubbo的springboot支持-->
        <dependency>
            <groupId>com.alibaba.boot</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
        </dependency>
        <!--dubbo框架-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dubbo</artifactId>
        </dependency>
        <!--zk依賴-->
        <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>
</project>

application.properties文件:

# Spring boot application
spring.application.name = itcast-tanhua-dubbo-es

# dubbo 掃描包配置
dubbo.scan.basePackages = com.tanhua.dubbo.es
dubbo.application.name = dubbo-provider-es

#dubbo 對外暴露的端口信息
dubbo.protocol.name = dubbo
dubbo.protocol.port = 20882

#dubbo注冊中心的配置
dubbo.registry.address = zookeeper://192.168.31.81:2181
dubbo.registry.client = zkclient
dubbo.registry.timeout = 60000 

#ES集群配置
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

啟動類:

package com.tanhua.dubbo.es;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;

@SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class}) //排除mongo的自動配置
public class ESApplication {

    public static void main(String[] args) {
        SpringApplication.run(ESApplication.class, args);
    }
}

1.1.2、定義pojo

在my-tanhua-dubbo-interface中創建:

package com.tanhua.dubbo.server.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.elasticsearch.common.geo.GeoPoint;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.GeoPointField;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "tanhua", type = "user_location", shards = 6, replicas = 2)
public class UserLocation {

    @Id
    private Long userId; //用戶id
    @GeoPointField
    private GeoPoint location; //lon:經度 lat:緯度

    @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; //上次更新時間
}

package com.tanhua.dubbo.server.vo;

import cn.hutool.core.bean.BeanUtil;
import com.tanhua.dubbo.server.pojo.UserLocation;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserLocationVo implements java.io.Serializable {

    private static final long serialVersionUID = 4133419501260037769L;

    private Long userId; //用戶id
    private Double longitude; //經度
    private Double latitude; //維度
    private String address; //位置描述
    private Long created; //創建時間
    private Long updated; //更新時間
    private Long lastUpdated; //上次更新時間

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

    public static final List<UserLocationVo> formatToList(List<UserLocation> userLocations) {
        List<UserLocationVo> list = new ArrayList<>();
        for (UserLocation userLocation : userLocations) {
            list.add(format(userLocation));
        }
        return list;
    }
}

由於UserLocation不能序列化,所以要再定義UserLocationVo進行返回數據。

在my-tanhua-dubbo-interface中添加依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
</dependency>

1.1.3、定義dubbo接口

在my-tanhua-dubbo-interface工程中。

package com.tanhua.dubbo.server.api;

public interface UserLocationApi {

    /**
     * 更新用戶地理位置
     *
     * @param userId 用戶id
     * @param longitude 經度
     * @param latitude 緯度
     * @param address 地址名稱
     * @return
     */
    Boolean updateUserLocation(Long userId, Double longitude, Double latitude, String address);

}

1.1.4、編寫實現

package com.tanhua.dubbo.es.api;

import cn.hutool.core.util.ObjectUtil;
import com.alibaba.dubbo.config.annotation.Service;
import com.tanhua.dubbo.server.api.UserLocationApi;
import com.tanhua.dubbo.server.pojo.UserLocation;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.common.geo.GeoPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.query.*;

import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;

@Service(version = "1.0.0")
@Slf4j
public class UserLocationApiImpl implements UserLocationApi {

    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;

    /**
     * 初始化索引庫
     *
     */
    @PostConstruct
    public void initIndex(){
        //判斷索引庫是否存在,如果不存在,需要創建
        if(!this.elasticsearchTemplate.indexExists("tanhua")){
            this.elasticsearchTemplate.createIndex(UserLocation.class);
        }

        //判斷表是否存在,如果不存在,需要創建
        if(!this.elasticsearchTemplate.typeExists("tanhua", "user_location")){
            this.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 = this.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();

                //保存數據到ES中
                this.elasticsearchTemplate.index(indexQuery);
            }else {
                //更新數據

                //更新的字段
                Map<String,Object> map = new HashMap<>();
                map.put("location", new GeoPoint(latitude, longitude));
                map.put("updated", System.currentTimeMillis());
                map.put("lastUpdated", userLocation.getUpdated());
                map.put("address", address);

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

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

                //更新數據
                this.elasticsearchTemplate.update(updateQuery);
            }

            return true;
        } catch (Exception e) {
            log.error("更新地理位置失敗~ userId = " + userId + ", longitude = " + longitude + ", latitude = " + latitude + ", address = " + address, e);
        }

        return false;
    }
}

1.1.5、單元測試

package com.tanhua.dubbo.es;

import com.tanhua.dubbo.server.api.UserLocationApi;
import com.tanhua.dubbo.server.vo.PageInfo;
import com.tanhua.dubbo.server.vo.UserLocationVo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestUserLocationApi {

    @Autowired
    private UserLocationApi userLocationApi;

    @Test
    public void testUpdateUserLocation() {
        this.userLocationApi.updateUserLocation(1L, 121.512253, 31.24094, "金茂大廈");
        this.userLocationApi.updateUserLocation(2L, 121.506377, 31.245105, "東方明珠廣播電視塔");
        this.userLocationApi.updateUserLocation(10L, 121.508815, 31.243844, "陸家嘴地鐵站");
        this.userLocationApi.updateUserLocation(12L, 121.511999, 31.239185, "上海中心大廈");
        this.userLocationApi.updateUserLocation(25L, 121.493444, 31.240513, "上海市公安局");
        this.userLocationApi.updateUserLocation(27L, 121.494108, 31.247011, "上海外灘美術館");
        this.userLocationApi.updateUserLocation(30L, 121.462452, 31.253463, "上海火車站");
        this.userLocationApi.updateUserLocation(32L, 121.81509, 31.157478, "上海浦東國際機場");
        this.userLocationApi.updateUserLocation(34L, 121.327908, 31.20033, "虹橋火車站");
        this.userLocationApi.updateUserLocation(38L, 121.490155, 31.277476, "魯迅公園");
        this.userLocationApi.updateUserLocation(40L, 121.425511, 31.227831, "中山公園");
        this.userLocationApi.updateUserLocation(43L, 121.594194, 31.207786, "張江高科");
    }

}

1.2、APP接口

接口文檔:https://mock-java.itheima.net/project/35/interface/api/557

1.2.1、BaiduController

package com.tanhua.server.controller;

import com.tanhua.server.service.BaiduService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("baidu")
public class BaiduController {

    @Autowired
    private BaiduService baiduService;

    /**
     * 更新位置
     *
     * @param param
     * @return
     */
    @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 bool = this.baiduService.updateLocation(longitude, latitude, address);
            if (bool) {
                return ResponseEntity.ok(null);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
}

1.2.2、BaiduService

package com.tanhua.server.service;

import com.alibaba.dubbo.config.annotation.Reference;
import com.tanhua.common.pojo.User;
import com.tanhua.common.utils.UserThreadLocal;
import com.tanhua.dubbo.server.api.UserLocationApi;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class BaiduService {

    @Reference(version = "1.0.0")
    private UserLocationApi userLocationApi;

    public Boolean updateLocation(Double longitude, Double latitude, String address) {
        User user = UserThreadLocal.get();
        try {
            return this.userLocationApi.updateUserLocation(user.getId(), longitude, latitude, address);
        } catch (Exception e) {
            log.error("更新地理位置失敗~ userId = " + user.getId() + ", longitude = " + longitude + ", latitude = " + latitude + ", address = " + address, e);
        }
        return false;
    }

}

1.3、測試

image-20210107230457781

2、搜附近

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

1571966080530

實現思路:根據當前用戶的位置,查詢附近范圍內的用戶。范圍是可以設置的。

2.1、dubbo服務

2.1.1、定義接口方法

//com.tanhua.dubbo.server.api.UserLocationApi

	/**
     * 查詢用戶地理位置
     *
     * @param userId
     * @return
     */
    UserLocationVo queryByUserId(Long userId);

    /**
     * 根據位置搜索
     *
     * @param longitude 經度
     * @param latitude  緯度
     * @param distance  距離(米)
     * @param page      頁數
     * @param pageSize  頁面大小
     */
    PageInfo<UserLocationVo> queryUserFromLocation(Double longitude, Double latitude, Double distance, Integer page, Integer pageSize);

2.1.2、編寫實現

//com.tanhua.dubbo.es.api.UserLocationApiImpl

	/**
     * 查詢用戶的位置信息
     *
     * @param userId
     * @return
     */
    @Override
    public UserLocationVo queryByUserId(Long userId) {

        GetQuery getQuery = new GetQuery();
        getQuery.setId(String.valueOf(userId));
        UserLocation userLocation = this.elasticsearchTemplate.queryForObject(getQuery, UserLocation.class);
        if(ObjectUtil.isNotEmpty(userLocation)){
            return UserLocationVo.format(userLocation);
        }

        return null;
    }

    /**
     * 根據位置搜索
     *
     * @param longitude 經度
     * @param latitude  緯度
     * @param distance  距離(米)
     * @param page      頁數
     * @param pageSize  頁面大小
     */
    @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);

        String fieldName = "location";

        //實現了SearchQuery接口,構造分頁、排序
        NativeSearchQueryBuilder searchQueryBuilder = new NativeSearchQueryBuilder();

        //分頁
        PageRequest pageRequest = PageRequest.of(page - 1, pageSize);
        searchQueryBuilder.withPageable(pageRequest);

        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();

        //以一個點為中心,指定范圍查詢
        GeoDistanceQueryBuilder geoDistanceQueryBuilder = new GeoDistanceQueryBuilder(fieldName);
        //中心點
        geoDistanceQueryBuilder.point(new GeoPoint(latitude, longitude));
        //距離(畫圓的半徑)單位:公里
        geoDistanceQueryBuilder.distance(distance / 1000, DistanceUnit.KILOMETERS);

        boolQueryBuilder.must(geoDistanceQueryBuilder);
        searchQueryBuilder.withQuery(boolQueryBuilder);

        //排序,由近到遠排序
        GeoDistanceSortBuilder geoDistanceSortBuilder = new GeoDistanceSortBuilder(fieldName, latitude, longitude);
        geoDistanceSortBuilder.order(SortOrder.ASC); //正序排序
        geoDistanceSortBuilder.unit(DistanceUnit.KILOMETERS); //設置單位
        searchQueryBuilder.withSort(geoDistanceSortBuilder);

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

        pageInfo.setRecords(UserLocationVo.formatToList(aggregatedPage.getContent()));

        return pageInfo;
    }

2.1.3、單元測試

//com.tanhua.dubbo.es.TestUserLocationApi

	@Test
    public void testQueryByUserId(){
        UserLocationVo userLocationVo = this.userLocationApi.queryByUserId(1L);
        System.out.println(userLocationVo);
    }

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

2.2、APP接口服務

文檔地址:https://mock-java.itheima.net/project/35/interface/api/611

2.2.1、NearUserVo

package com.tanhua.server.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class NearUserVo {

    private Long userId;
    private String avatar;
    private String nickname;

}

2.2.2、TanHuaController

//com.tanhua.server.controller.TanHuaController

	/**
     * 搜附近
     *
     * @param gender
     * @param distance
     * @return
     */
    @GetMapping("search")
    public ResponseEntity<List<NearUserVo>> queryNearUser(@RequestParam(value = "gender", required = false) String gender,
                                                          @RequestParam(value = "distance", defaultValue = "2000") String distance) {
        try {
            List<NearUserVo> list = this.tanHuaService.queryNearUser(gender, distance);
            return ResponseEntity.ok(list);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }

2.2.3、TanHuaService

//com.tanhua.server.service.TanHuaService

    public List<NearUserVo> queryNearUser(String gender, String distance) {
        //查詢當前用戶的位置
        User user = UserThreadLocal.get();
        UserLocationVo userLocationVo = this.userLocationApi.queryByUserId(user.getId());
        if(ObjectUtil.isEmpty(userLocationVo)){
            return ListUtil.empty();
        }

        PageInfo<UserLocationVo> pageInfo = this.userLocationApi.queryUserFromLocation(userLocationVo.getLongitude(),
                userLocationVo.getLatitude(),
                Convert.toDouble(distance),
                1,
                50
        );

        List<UserLocationVo> records = pageInfo.getRecords();
        if(CollUtil.isEmpty(records)){
            return ListUtil.empty();
        }

        //構造篩選條件
        List<Object> userIdList = CollUtil.getFieldValues(records, "userId");
        QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.in("user_id", userIdList);
        if(StrUtil.equalsIgnoreCase(gender, "man")){
            queryWrapper.eq("sex", SexEnum.MAN);
        }else if(StrUtil.equalsIgnoreCase(gender, "woman")){
            queryWrapper.eq("sex", SexEnum.WOMAN);
        }

        List<UserInfo> userInfoList = this.userInfoService.queryUserInfoList(queryWrapper);

        List<NearUserVo> result = new ArrayList<>();
        for (UserLocationVo locationVo : records) {
            //排除自己
            if(ObjectUtil.equals(locationVo.getUserId(), user.getId())){
                continue;
            }

            for (UserInfo userInfo : userInfoList) {
                if(ObjectUtil.equals(locationVo.getUserId(), userInfo.getUserId())){

                    NearUserVo nearUserVo = new NearUserVo();
                    nearUserVo.setUserId(userInfo.getUserId());
                    nearUserVo.setAvatar(userInfo.getLogo());
                    nearUserVo.setNickname(userInfo.getNickName());
                    result.add(nearUserVo);
                    break;
                }
            }
        }

        return result;
    }

2.2.4、測試

image-20210114102844696

3、探花

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

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

1572105631737

如果已經喜歡或不喜歡的用戶在列表中不再顯示。

3.1、喜歡的dubbo服務

用戶的喜歡與不喜歡列表需要保存在redis中,為了防止redis中的數據丟失,同時需要將數據保存到mongodb進行持久化保存。

3.1.1、實體對象

package com.tanhua.dubbo.server.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;

@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 Long created; //創建時間

}

3.1.2、定義接口

package com.tanhua.dubbo.server.api;

import java.util.List;

public interface UserLikeApi {

    /**
     * 喜歡
     *
     * @param userId
     * @param likeUserId
     * @return
     */
    Boolean likeUser(Long userId, Long likeUserId);

    /**
     * 不喜歡
     *
     * @param userId
     * @param likeUserId
     * @return
     */
    Boolean notLikeUser(Long userId, Long likeUserId);


    /**
     * 是否相互喜歡
     *
     * @param userId
     * @param likeUserId
     * @return
     */
    Boolean isMutualLike(Long userId, Long likeUserId);


    /**
     * 查詢喜歡列表
     *
     * @param userId
     * @return
     */
    List<Long> queryLikeList(Long userId);

    /**
     * 查詢不喜歡列表
     *
     * @param userId
     * @return
     */
    List<Long> queryNotLikeList(Long userId);

}

3.1.3、編寫實現

package com.tanhua.dubbo.server.api;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.convert.Convert;
import com.alibaba.dubbo.config.annotation.Service;
import com.tanhua.dubbo.server.pojo.UserLike;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@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_";


    /**
     * 喜歡
     *
     * @param userId
     * @param likeUserId
     * @return
     */
    @Override
    public Boolean likeUser(Long userId, Long likeUserId) {
        //判斷該用戶是否已經喜歡,如果已經喜歡就返回
        if(this.isLike(userId, likeUserId)){
            return false;
        }

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

        //將數據存儲到MongoDB
        this.mongoTemplate.save(userLike);

        //用戶的喜歡數據保存到redis
        //用戶1:key -> USER_LIKE_1 , value -> 2, "1"
        //用戶1:key -> USER_LIKE_1 , value -> 3, "1"
        //用戶2:key -> USER_LIKE_2 , value -> 4, "1"
        String redisKey = this.getLikeRedisKey(userId);
        String hashKey = String.valueOf(likeUserId);
        this.redisTemplate.opsForHash().put(redisKey, hashKey, "1");

        //判斷,喜歡的用戶是否在不喜歡的列表中,如果在,就需要刪除數據
        if(this.isNotLike(userId, likeUserId)){
            redisKey = this.getNotLikeRedisKey(userId);
            this.redisTemplate.opsForHash().delete(redisKey, hashKey);
        }

        return true;
    }

    /**
     * 獲取喜歡數據的redis key
     *
     * @param userId
     * @return
     */
    private String getLikeRedisKey(Long userId){
        return LIKE_REDIS_KEY_PREFIX + userId;
    }

    /**
     * 獲取不喜歡數據的redis key
     *
     * @param userId
     * @return
     */
    private String getNotLikeRedisKey(Long userId){
        return NOT_LIKE_REDIS_KEY_PREFIX + userId;
    }

    /**
     * 是否喜歡
     *
     * @param userId
     * @param likeUserId
     * @return
     */
    private Boolean isLike(Long userId, Long likeUserId){
        String redisKey = this.getLikeRedisKey(userId);
        String hashKey = String.valueOf(likeUserId);
        return this.redisTemplate.opsForHash().hasKey(redisKey, hashKey);
    }

    /**
     * 是否不喜歡
     *
     * @param userId
     * @param likeUserId
     * @return
     */
    private Boolean isNotLike(Long userId, Long likeUserId){
        String redisKey = this.getNotLikeRedisKey(userId);
        String hashKey = String.valueOf(likeUserId);
        return this.redisTemplate.opsForHash().hasKey(redisKey, hashKey);
    }

    @Override
    public Boolean notLikeUser(Long userId, Long likeUserId) {
        //判斷用戶是否已經不喜歡,如果已經不喜歡,就返回
        if(this.isNotLike(userId, likeUserId)){
            return false;
        }

        //將用戶保存到不喜歡列表中
        String redisKey = this.getNotLikeRedisKey(userId);
        String hashKey = String.valueOf(likeUserId);
        this.redisTemplate.opsForHash().put(redisKey, hashKey, "1");

        //判斷用戶是否在喜歡列表中,如果存在的話,需要刪除數據
        if(this.isLike(userId, likeUserId)){
            //刪除MongoDB數據
            Query query = Query.query(Criteria
                    .where("userId").is(userId)
                    .and("likeUserId").is(likeUserId)
            );
            this.mongoTemplate.remove(query, UserLike.class);

            //刪除redis中的數據
            redisKey = this.getLikeRedisKey(userId);
            this.redisTemplate.opsForHash().delete(redisKey, hashKey);
        }

        return true;
    }

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

    @Override
    public List<Long> queryLikeList(Long userId) {
        // 查詢redis
        String redisKey = this.getLikeRedisKey(userId);
        Set<Object> keys = this.redisTemplate.opsForHash().keys(redisKey);
        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) {
        // 查詢redis
        String redisKey = this.getNotLikeRedisKey(userId);
        Set<Object> keys = this.redisTemplate.opsForHash().keys(redisKey);
        if(CollUtil.isEmpty(keys)){
            return ListUtil.empty();
        }

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

3.1.4、單元測試

package com.tanhua.dubbo.server.api;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestUserLikeApi {

    @Autowired
    private UserLikeApi userLikeApi;

    @Test
    public void testUserLike() {
        System.out.println(this.userLikeApi.likeUser(1L, 2L));
        System.out.println(this.userLikeApi.likeUser(1L, 3L));
        System.out.println(this.userLikeApi.likeUser(1L, 4L));

        System.out.println(this.userLikeApi.notLikeUser(1L, 5L));
        System.out.println(this.userLikeApi.notLikeUser(1L, 6L));

        System.out.println(this.userLikeApi.likeUser(1L, 5L));
        System.out.println(this.userLikeApi.notLikeUser(1L, 2L));
    }

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

3.2、查詢推薦列表dubbo服務

3.2.1、定義接口

//com.tanhua.dubbo.server.api.RecommendUserApi

/**
 * 查詢探花列表,查詢時需要排除不喜歡列表用戶
 *
 * @param userId
 * @param count
 * @return
 */
List<RecommendUser> queryCardList(Long userId, Integer count);

3.2.2、編寫實現

//com.tanhua.dubbo.server.api.RecommendUserApiImpl

    @Override
    public List<RecommendUser> queryCardList(Long userId, Integer count) {
        //設置分頁以及排序,按照得分倒序排序
        PageRequest pageRequest = PageRequest.of(0, count, Sort.by(Sort.Order.desc("score")));

        //排除已喜歡或不喜歡的用戶
        List<Long> userIds = new ArrayList<>();
        //查詢喜歡列表
        userIds.addAll(this.userLikeApi.queryLikeList(userId));

        //查詢不喜歡列表
        userIds.addAll(this.userLikeApi.queryNotLikeList(userId));

        //構造查詢條件
        Criteria criteria = Criteria.where("toUserId").is(userId);
        if(CollUtil.isNotEmpty(userIds)){
            //加入到查詢條件中,排除這些用戶
            criteria.andOperator(Criteria.where("userId").nin(userIds));
        }

        Query query = Query.query(criteria).with(pageRequest);
        List<RecommendUser> recommendUserList = this.mongoTemplate.find(query, RecommendUser.class);
        return recommendUserList;
    }

3.2.3、單元測試

//com.tanhua.dubbo.server.api.TestRecommendUserApi

@Test
public void testQueryCardList(){
    this.recommendUserApi.queryCardList(2L, 20)
            .forEach(recommendUser -> System.out.println(recommendUser));
}

3.3、查詢推薦列表APP接口實現

接口文檔:https://mock-java.itheima.net/project/35/interface/api/593

3.3.1、TanHuaController


	/**
     * 探花
     *
     * @return
     */
    @GetMapping("cards")
    public ResponseEntity<List<TodayBest>> queryCardsList() {
        try {
            List<TodayBest> list = this.tanHuaService.queryCardsList();
            return ResponseEntity.ok(list);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }

3.3.2、TanHuaService

#默認推薦列表
tanhua.default.recommend.users=2,3,8,10,18,20,24,29,27,32,36,37,56,64,75,88
/**
 * 查詢推薦卡片列表,從推薦列表中隨機選取10個用戶
 *
 * @return
 */
    public List<TodayBest> queryCardsList() {

        User user = UserThreadLocal.get();
        int count = 50;

        //查詢到的50條數據,並不是用來直接展現,需要從這50條數據中隨機返回一些數據
        List<RecommendUser> recommendUserList = this.recommendUserService.queryCardList(user.getId(), count);
        if (CollUtil.isEmpty(recommendUserList)) {
            recommendUserList = new ArrayList<>();
            //默認推薦列表
            List<String> list = StrUtil.split(defaultRecommendUsers, ',');
            for (String userId : list) {
                RecommendUser recommendUser = new RecommendUser();

                recommendUser.setToUserId(user.getId());
                recommendUser.setUserId(Convert.toLong(userId));
                recommendUserList.add(recommendUser);
            }
        }

        //計算展現的數量,默認展現10個
        int showCount = Math.min(10, recommendUserList.size());
        List<RecommendUser> result = new ArrayList<>();
        for (int i = 0; i < showCount; i++) {
            //TODO 可能重復
            int index = RandomUtil.randomInt(0, recommendUserList.size());
            RecommendUser recommendUser = recommendUserList.get(index);
            result.add(recommendUser);
        }

        List<Object> userIdList = CollUtil.getFieldValues(result, "userId");
        List<UserInfo> userInfoList = this.userInfoService.queryUserInfoByUserIdList(userIdList);
        List<TodayBest> todayBests = new ArrayList<>();
        for (UserInfo userInfo : userInfoList) {
            TodayBest todayBest = new TodayBest();
            todayBest.setId(userInfo.getUserId());
            todayBest.setAge(userInfo.getAge());
            todayBest.setAvatar(userInfo.getLogo());
            todayBest.setGender(userInfo.getSex().name().toLowerCase());
            todayBest.setNickname(userInfo.getNickName());
            todayBest.setTags(Convert.toStrArray(StrUtil.split(userInfo.getTags(), ',')));
            todayBest.setFateValue(0L);

            todayBests.add(todayBest);
        }

        return todayBests;
    }

3.3.3、測試

image-20210114162852839

image-20210114162907596

效果:

image-20210114163241129

3.4、左滑右滑

左滑:“不喜歡”,右滑:“喜歡”,如果雙方喜歡,那么就會成為好友。

喜歡的接口文檔:https://mock-java.itheima.net/project/35/interface/api/599

不喜歡的接口文檔:https://mock-java.itheima.net/project/35/interface/api/605

3.4.1、TanHuaController

/**
 * 喜歡
 *
 * @param likeUserId
 * @return
 */
@GetMapping("{id}/love")
public ResponseEntity<Void> likeUser(@PathVariable("id") Long likeUserId) {
    try {
        this.tanHuaService.likeUser(likeUserId);
        return ResponseEntity.ok(null);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

/**
 * 不喜歡
 *
 * @param likeUserId
 * @return
 */
@GetMapping("{id}/unlove")
public ResponseEntity<Void> notLikeUser(@PathVariable("id") Long likeUserId) {
    try {
        this.tanHuaService.notLikeUser(likeUserId);
        return ResponseEntity.ok(null);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

3.4.2、TanHuaService

//com.tanhua.server.service.TanHuaService

public Boolean likeUser(Long likeUserId) {
    User user = UserThreadLocal.get();
    Boolean result = this.userLikeApi.likeUser(user.getId(), likeUserId);
    if (!result) {
        return false;
    }

    if (this.userLikeApi.isMutualLike(user.getId(), likeUserId)) {
        //相互喜歡成為好友
        this.imService.contactUser(likeUserId);
    }
    return true;
}

public Boolean notLikeUser(Long likeUserId) {
    User user = UserThreadLocal.get();
    return this.userLikeApi.notLikeUser(user.getId(), likeUserId);
}

3.4.3、測試

image-20210114165501156

image-20210114165651535

user_like表,可以看到已經相互喜歡了:

image-20210114165721204

tanhua_users表,可以看到相互是好友了:

image-20210114170129360

環信平台:

image-20210114170153850

4、用戶資料

在我的中心模塊中,可以對個人信息做修改。

image-20210114203734594

4.1、基本信息

在前面實現的查詢個人信息接口中,已經返回個人基本數據,所以可以直接展現出個人信息,下面只需要進行實現數據的保存即可。

4.4.1、接口信息

接口地址:https://mock-java.itheima.net/project/35/interface/api/887

image-20210114204120298

請求參數:

image-20210114204151930

4.4.2、MyCenterController

//com.tanhua.server.controller.MyCenterController

/**
 * 更新用戶信息
 *
 * @param userInfoVo
 * @return
 */
@PutMapping
public ResponseEntity<Void> updateUserInfo(@RequestBody UserInfoVo userInfoVo){
    try {
        Boolean bool = this.myCenterService.updateUserInfo(userInfoVo);
        if(bool){
            return ResponseEntity.ok(null);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

4.4.3、MyCenterService

//com.tanhua.server.service.MyCenterService

public Boolean updateUserInfo(UserInfoVo userInfoVo) {
    User user = UserThreadLocal.get();
    UserInfo userInfo = new UserInfo();
    userInfo.setUserId(user.getId());
    userInfo.setAge(Integer.valueOf(userInfoVo.getAge()));
    userInfo.setSex(StringUtils.equalsIgnoreCase(userInfoVo.getGender(), "man") ? SexEnum.MAN : SexEnum.WOMAN);
    userInfo.setBirthday(userInfoVo.getBirthday());
    userInfo.setCity(userInfoVo.getCity());
    userInfo.setEdu(userInfoVo.getEducation());
    userInfo.setIncome(StringUtils.replaceAll(userInfoVo.getIncome(), "K", ""));
    userInfo.setIndustry(userInfoVo.getProfession());
    userInfo.setMarriage(userInfoVo.getMarriage() == 1 ? "已婚" : "未婚");
    return this.userInfoService.updateUserInfoByUserId(userInfo);
}

4.4.4、UserInfoService

//com.tanhua.server.service.UserInfoService

public boolean updateUserInfoByUserId(UserInfo userInfo) {
    QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("user_id", userInfo.getUserId());
    return this.userInfoMapper.update(userInfo, queryWrapper) > 0;
}

4.4.5、bug修復

在之前的查詢個人信息中接口中,返回數據中的性別數據有誤,需要返回man或woman。

如下:

//com.tanhua.server.service.MyCenterService

	public UserInfoVo queryUserInfoByUserId(Long userId) {
        if (ObjectUtil.isEmpty(userId)) {
            //如果查詢id為null,就表示查詢當前用戶信息
            userId = UserThreadLocal.get().getId();
        }
        //查詢用戶信息
        UserInfo userInfo = this.userInfoService.queryUserInfoByUserId(userId);
        if (ObjectUtil.isEmpty(userInfo)) {
            return null;
        }

        UserInfoVo userInfoVo = BeanUtil.copyProperties(userInfo, UserInfoVo.class, "marriage");
        userInfoVo.setGender(userInfo.getSex().getValue() == 1 ? "man" : "women");
        userInfoVo.setMarriage(StrUtil.equals("已婚", userInfo.getMarriage()) ? 1 : 0);
        return userInfoVo;
    }

4.2、更新頭像

上傳頭像使用sso中的上傳邏輯即可,只是路徑不同,所以我們只需要修改nginx配置和在sso中定義Controller即可。

接口文檔:https://mock-java.itheima.net/project/35/interface/api/881

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        location /user/ {  #請求路徑中凡是以/user/開頭的請求,轉發到sso系統
            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;  #轉發請求
        }

        location /users/header {  #請求路徑中凡是以/user/header開頭的請求,轉發到sso系統
            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;  #轉發請求
        }

        location / {   #上面未匹配到的在這里處理
            client_max_body_size  300m;
            proxy_connect_timeout 300s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;
                proxy_pass   http://127.0.0.1:18081;  #轉發請求到server系統
            }
        }

}

4.2.2、MyCenterController

在sso工程中定義MyCenterController。

package com.tanhua.sso.controller;

import com.tanhua.sso.vo.ErrorResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("users")
public class MyCenterController {

    @Autowired
    private UserInfoController userInfoController;

    /**
     * 上傳頭像
     *
     * @param file
     * @param token
     * @return
     */
    @PostMapping("header")
    public ResponseEntity<Object> saveLogo(@RequestParam("headPhoto") MultipartFile file, @RequestHeader("Authorization") String token) {
        return this.userInfoController.saveUserLogo(file, token);
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM