美食社交--好友服务

需求说明

好友功能 是目前社交场景必备功能之一,一般好友相关功能包含 : 关注/取关, 我(他)的关注或者粉丝,共同关注,我关注的人也关注他等等。
image.png
方式一 : 数据库 : 只能得到粉丝列表或者 关注列表,如果想查多用户的共同关注或者共同粉丝比较麻烦,低效
方式二 : Redis : 操作简单效率高。redis 本身带有专门针对这种集合的交集、并集、差集的操作。

设计思路

总体思路 采用 MySQL + Redis 方式结合完成。
MySQL 用于保存落地数据,Redis 的 Sets进行集合操作。Sets拥有去重(我们不能多次关注同一用户)功能。一个用户我们存贮两个集合:一个是保存用户关注的人 另一个是保存关注用户的人.
SADD 添加成员;命令格式: SADD key member [member …] ----- 关注
SREM 移除某个成员;命令格式: SREM key member [member …] -------取关
SCARD 统计集合内的成员数;命令格式: SCARD key -------关注/粉丝个数
SISMEMBER 判断是否是集合成员;命令格式:SISMEMBER key member ---------判断是否关注(如果是只可以点击取关)
SMEMBERS 查询集合内的成员;命令格式: SMEMBERS key -------列表使用(关注列表和粉丝列表)
SINTER 查询集合的交集;命令格式: SINTER key [key …] --------共同关注、我关注的人关注了他

表结构

建模块

pom依赖

<?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>food_social</artifactId>
        <groupId>com.itkaka</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>fs_follow</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- eureka client -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- spring web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- spring data redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!-- commons 公共项目 -->
        <dependency>
            <groupId>com.itkaka</groupId>
            <artifactId>fs_commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <!-- 集中定义项目所需插件 -->
    <build>
        <plugins>
            <!-- spring boot maven 项目打包插件 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

配置文件

server:
  port: 8094 # 端口

spring:
  application:
    name: fs_follow # 应用名
  # 数据库
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://127.0.0.1:3306/db_lezijie_food_social?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false
  # Redis
#  redis:
#    port: 6379
#    host: 192.168.10.101
#    timeout: 3000
#    password: 123456
#    database: 2
  # Swagger
  swagger:
    base-package: com.itkaka.follow
    title: 美食社交好友API接口文档

# 配置 Eureka Server 注册中心
eureka:
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
  client:
    service-url:
      defaultZone: http://localhost:8090/eureka/

service:
  name:
    fs-oauth-server: http://fs_oauth/
    fs-diners-server: http://fs_diners/
    fs-feeds-server: http://fs_feeds/

mybatis:
  configuration:
    map-underscore-to-camel-case: true # 开启驼峰映射

# 配置日志
logging:
  pattern:
    console: '%d{2100-01-01 13:14:00.666} [%thread] %-5level %logger{50} - %msg%n'

关注 / 取关

关注/取消关注 业务逻辑

image.png

实体类

package com.itkaka.follow.model.pojo;

import com.itkaka.commons.model.base.BaseModel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;

@ApiModel(description = "食客关注实体类")
@Getter
@Setter
public class Follow extends BaseModel { // 需要继承 BaseModel
    @ApiModelProperty("当前登录用户的ID")
    private Integer dinerId; // 包装类型默认值
    @ApiModelProperty("被关注者的ID")
    private Integer followDinerId;

}

配置类

package com.itkaka.follow.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisTemplateConfiguration {
    /**
     * redisTemplate 序列化使用的jdkSerializeable, 存储二进制字节码, 所以自定义序列化类
     *
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        //在这里形参报错是因为我的主启动类还没有写好,所以idea编译器检测不到这是一个springboot项目,所以无法通过形参注入成功
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 使用Jackson2JsonRedisSerialize 替换默认序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 设置key和value的序列化规则
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }


}

package com.itkaka.follow.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
 * Rest 配置类
 */
@Configuration
public class RestTemplateConfiguration {

    @LoadBalanced
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

}

全局异常
package com.itkaka.follow.handler;

import com.itkaka.commons.exception.ParameterException;
import com.itkaka.commons.model.domain.ResultInfo;
import com.itkaka.commons.utils.ResultInfoUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;

@RestControllerAdvice // 将输出的内容写入 ResponseBody 中
@Slf4j
public class GlobalExceptionHandler {
    @Resource
    private HttpServletRequest request;

    @ExceptionHandler(ParameterException.class)
    public ResultInfo<Map<String,String>> handlerParameterException(ParameterException ex){
        String path = request.getRequestURI();
        ResultInfo<Map<String,String>> resultInfo =
                ResultInfoUtil.buildError(ex.getErrorCode(),ex.getMessage(),path);
        return resultInfo;
    }

    @ExceptionHandler(Exception.class)
    public ResultInfo<Map<String,String>> handlerException(Exception ex){
        log.info("未知异常: {}",ex);
        String path = request.getRequestURI();
        ResultInfo<Map<String,String>> resultInfo =
                ResultInfoUtil.buildError(path);
        return resultInfo;
    }

}

持久层

package com.itkaka.follow.mapper;

import com.itkaka.follow.model.pojo.Follow;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

/*
*  好友服务
* */
public interface FollowMapper {

    //查询关注信息
    @Select("select id,diner_id,follow_diner_id,is_valid "+
    " from t_follow where diner_id = #{dinerId} and follow_diner_id = #{follow_diner_id}")
    Follow selectFollow(@Param("dinerId") Integer dinerId,
                        @Param("followDinerId") Integer followDinerId);

    //添加关注信息
    @Insert("insert into t_follow (diner_id, follow_diner_id, is_valid, create_date, update_date) " +
    "values (#{dinerId}, #{followDinerId}, 1, now(), now())")
    int save(@Param("dinerId") Integer dinerId,
             @Param("followDinerId") Integer followDinerId);

    //修改关注信息
    @Update("update t_follow set is_valid = #{isFollowed},update_date = now() where id = #{id}")
    int update(@Param("isFollowed") Integer isFollowed,@Param("id") Integer id);

}

业务层

package com.itkaka.follow.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.itkaka.commons.constant.ApiConstant;
import com.itkaka.commons.constant.RedisKeyConstant;
import com.itkaka.commons.exception.ParameterException;
import com.itkaka.commons.model.domain.ResultInfo;
import com.itkaka.commons.model.vo.ShortDinerInfo;
import com.itkaka.commons.model.vo.SignInDinerInfo;
import com.itkaka.commons.utils.AssertUtil;
import com.itkaka.commons.utils.ResultInfoUtil;
import com.itkaka.follow.mapper.FollowMapper;
import com.itkaka.follow.model.pojo.Follow;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;

/*
*  好友服务
* */
@Service
public class FollowService {

    @Value("${service.name.fs_oauth-server}")
    private String oauthServerName;
    @Value("${service.name.fs_diners-server}")
    private String dinersServerName;

    // feed服务
    @Value("${service.name.fs_feeds-server}")
    private String feedsServerName;

    @Resource
    private RestTemplate restTemplate;
    @Resource
    private RedisTemplate redisTemplate;
    @Resource
    private FollowMapper followMapper;

    //关注/取关
    @Transactional(rollbackFor = Exception.class)
    public ResultInfo follow(String accessToken,Integer followDinerId,
                             int isFollowed,String path){
        //是否选择关注对象
        AssertUtil.isTrue(followDinerId == null || followDinerId <1,
                "请选择要关注的人");
        // 获取登录用户信息
        SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
        //获取当前登录用户和需要关注用户的关注信息
        Follow follow = followMapper.selectFollow(dinerInfo.getId(),followDinerId);
        //如果没有关注信息,且要关注操作
        if (follow == null && isFollowed == 1){
            //加关注信息
            int count = followMapper.save(dinerInfo.getId(),followDinerId);
            //把关注信息添加到 redis
            if (count == 1){
                addToRedisSet(dinerInfo.getId(),followDinerId);
                //处理 Reed

            }
            return ResultInfoUtil.build(ApiConstant.SUCCESS_CODE,"关注成功",path,"关注成功");
        }

        /*
        * 如果有关注信息,且 is_valid= 0 那么是取关
        * 如果有关注信息,且 is_valid= 1 那么是关注中
        * */
        //如果有关注信息,且目前处于取关状态,要进行关注操作
        if (follow != null && follow.getIsValid() == 0 && isFollowed == 1){
            //重新关注
            int count = followMapper.update(isFollowed,follow.getId()); // 采用修改操作
            // 加到 redis 关注集合
            if ( count == 1){
                addToRedisSet(dinerInfo.getId(),followDinerId);
                // 处理 feed

            }
            return ResultInfoUtil.build(ApiConstant.SUCCESS_CODE,"关注成功",path,"关注成功");
        }

        //如果有关注信息,目前处于关注中状态,且要进行取关操作
        if (follow != null && follow.getIsValid() == 1 && isFollowed == 0){
            //取关
            int count = followMapper.update(isFollowed,follow.getId());
            // 移除 redis 里面的关注信息
            if (count == 1){
                removeFromRedisSet(dinerInfo.getId(),followDinerId);
                // 处理 feed

            }
            return ResultInfoUtil.build(ApiConstant.SUCCESS_CODE, "取关成功", path, "取关成功");
        }
        return ResultInfoUtil.buildSuccess(path,"操作成功");
    }

    // 移除 Redis 中关注信息
    private void removeFromRedisSet(Integer dinerId, Integer followDinerId) {
        redisTemplate.opsForSet().remove(RedisKeyConstant.following.getKey() + dinerId,followDinerId);
        redisTemplate.opsForSet().remove(RedisKeyConstant.followers.getKey()+followDinerId,dinerId);
    }

    // 获取登录用户信息
    private SignInDinerInfo loadSignInDinerInfo(String accessToken) {
        // 判断是否有 accessToken
        AssertUtil.mustLogin(accessToken);
        // 拼接远程请求URL
        String url = oauthServerName + "user/me?access_token={accessToken}";
        // 发请求
        ResultInfo resultInfo = restTemplate.getForObject(url,ResultInfo.class,accessToken);
        if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE){
            throw new ParameterException(resultInfo.getCode(),resultInfo.getMessage());
        }
        SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(),new SignInDinerInfo(),false);
        return dinerInfo;
    }

    // 添加关注信息到 Redis 关注集合中
    private void addToRedisSet(Integer dinerId, Integer followDinerId) {
        redisTemplate.opsForSet().add(RedisKeyConstant.following.getKey()+dinerId,followDinerId);
        redisTemplate.opsForSet().add(RedisKeyConstant.followers.getKey()+followDinerId,dinerId);
    }

    // 获取共同关注列表
    public ResultInfo findCommonsFriends(Integer dinerId,String accessToken,String path){
        // 是否选择关注对象
        AssertUtil.isTrue(dinerId == null || dinerId <1,"请选择要查看的人");
        //获取登录用户信息
        SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
        // 获取登录用户关注信息
        String loginDinerKey = RedisKeyConstant.following.getKey() + dinerInfo.getId();
        // 获取查看用户的关注信息
        String dinerKey = RedisKeyConstant.following.getKey() + dinerId;
        // 计算交集
        Set<Integer> followingDinerIds = redisTemplate.opsForSet().intersect(loginDinerKey,dinerKey);
        // 没有交集
        if (followingDinerIds == null || followingDinerIds.isEmpty()){
            return ResultInfoUtil.buildSuccess(path,new ArrayList<ShortDinerInfo>());
        }
        // 有交集
        // 根据 ids 查询食客信息
        ResultInfo resultInfo = restTemplate.getForObject(dinersServerName + "findByIds?access_token={accessToken}&ids={ids}",
                ResultInfo.class,accessToken, StrUtil.join(",", followingDinerIds));
        // 处理结果集
                List<LinkedHashMap> dinerInfoMaps = (List<LinkedHashMap>) resultInfo.getData();
                List<ShortDinerInfo> dinerInfos = dinerInfoMaps.stream()
                        .map(diner -> BeanUtil.fillBeanWithMap(diner, new ShortDinerInfo(), false))
                        .collect(Collectors.toList());
                return ResultInfoUtil.buildSuccess(path, dinerInfos);
    }

    // 获取粉丝列表
    public Set<Integer> findFollwers(Integer dinerId){
        AssertUtil.isNotNull(dinerId,"请选择要查看的用户");
        Set<Integer> followers = redisTemplate.opsForSet()
                .members(RedisKeyConstant.followers.getKey() + dinerId);
        return followers;
    }

    /**
     * 发送请求添加或移除关注人的Feed列表
     *
     * @param followDinerId
     * @param accessToken
     * @param type
     */
    private void sendSaveOrRemoveFeed(Integer followDinerId, String accessToken, int type) {
        String url = feedsServerName + "updateFollowingFeeds/" + followDinerId +
                "?access_token=" + accessToken;
        // 构建请求头
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        // 构建请求体
        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
        body.add("type", type);
        HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
        restTemplate.postForEntity(url, entity, ResultInfo.class);
    }

}

控制层

package com.itkaka.follow.controller;

import com.itkaka.commons.model.domain.ResultInfo;
import com.itkaka.commons.utils.ResultInfoUtil;
import com.itkaka.follow.service.FollowService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Set;

/*
* 好友服务
* */
@RestController
public class FollowController {
    @Resource
    private FollowService followService;
    @Resource
    private HttpServletRequest request;

    // 关注/取关
    @PostMapping("{followDinerId}")
    public ResultInfo follow(@PathVariable Integer followDinerId,
                             @RequestParam int isFollowed,
                             String access_token){
        return followService.follow(access_token,followDinerId,isFollowed, request.getServletPath());
    }

    //获取共同关注列表
    @GetMapping("commons/{dinerId}")
    public ResultInfo findCommonsFriends(@PathVariable Integer dinerId,String access_token){
        return followService.findCommonsFriends(dinerId,access_token, request.getServletPath());
    }


    //获取粉丝列表
    @GetMapping
    public ResultInfo findFollowers(@PathVariable Integer dinerId){
        Set<Integer> followers = followService.findFollwers(dinerId);
        return ResultInfoUtil.buildSuccess(request.getServletPath(),followers);
    }

}

启动类

package com.itkaka.follow;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("com.itkaka.follow.mapper")
@SpringBootApplication
public class FollowApplication {

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

}

网关配置

        - id: fs_follow
          uri: lb://fs_follow
          predicates:
            - Path=/follow/**
          filters:
            - StripPrefix=1
server:
  port: 80 # 端口

spring:
  application:
    name: fs_gateway # 应用名
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启配置注册中心进行路由功能
          lower-case-service-id: true # 将服务名称转小写
      routes:
        - id: fs_diners
          uri: lb://fs_diners
          predicates:
            - Path=/diners/**
          filters:
            - StripPrefix=1

        - id: fs_oauth
          uri: lb://fs_oauth
          predicates:
            - Path=/auth/**
          filters:
            - StripPrefix=1

        - id: fs_seckill
          uri: lb://fs_seckill
          predicates:
            - Path=/seckill/**
          filters:
            - StripPrefix=1

        - id: fs_follow
          uri: lb://fs_follow
          predicates:
            - Path=/follow/**
          filters:
            - StripPrefix=1

测试PostMan

共同关注列表

  • 从Redis中读取登录用户的关注列表与查看用户的关注列表,然后进行交集操作,获取共同关注的用户id
  • 然后通过食客服务传入用户id数据获取用户基本信息

用户服务查询用户信息

添加视图对象
package com.itkaka.commons.model.vo;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;

import java.io.Serializable;

@ApiModel(description = "关注食客信息")
@Getter
@Setter
public class ShortDinerInfo implements Serializable {

    @ApiModelProperty("主键")
    private Integer id;
    
    @ApiModelProperty("昵称")
    private String nickname;
    
    @ApiModelProperty("头像")
    private String avatarUrl;

}

根据用户的ids(多个以逗号分隔)
三层代码具体见上面的三层代码块;
测试
image.png

写在最后

好友功能
这个功能中我们实现了关注、取关、获取共同关注列表功能。
这个功能中 Redis 主要用于存储每个用户关注好友添加的 Feed 流集合,使用了 Sorted Set 数据类型。

👉 💕美好的一周,从周一开始,一起加油!后续持续更新,码字不易,麻烦大家小手一点 , 点赞或关注 , 感谢大家的支持!! ☀

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值