好友功能是目前社交场景的必备功能之一,一般好友相关的功能包含有:关注/取关、我(他)的关注、我(他)的粉丝、共同关注等这样一些功能。
总体思路我们采用MySQL + Redis的方式结合完成。MySQL主要是保存落地数据,而利用Redis的Sets进行集合操作。Sets拥有去重(我们不能多次关注同一用户)功能**。一个用户我们存贮两个集合:一个是保存用户关注的人 另一个是保存关注用户的人.**
CREATE TABLE `t_follow` (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`diner_id` int(11) NULL DEFAULT NULL COMMENT '用户id' ,
`follow_diner_id` int(11) NULL DEFAULT NULL COMMENT '用户关注的人id' ,
`is_valid` tinyint(1) NULL DEFAULT NULL COMMENT '是否关注,0未关注/取消关注,1已关注',
`create_date` datetime NULL DEFAULT NULL ,
`update_date` datetime NULL DEFAULT NULL ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci
AUTO_INCREMENT=6
ROW_FORMAT=COMPACT;
为什么t_follow要使用is_valid标识符?
答:当用户A第一次关注用户B时,会往t_follow表添加一条数据,此时 is_valid值为1,当A取消对B的关注时,不需要删除刚刚插入的数据,只需要更改is_valid变成0即可。当然也可以不用is_valid字段,当A取消对B的关注时,物理删除对应的这条数据,但公司一般删除不会做物理删除。
@Service
public class FollowService {
@Value("${service.name.ms-oauth-server}")
private String oauthServerName;
@Resource
private RestTemplate restTemplate;
@Resource
private FollowMapper followMapper;
@Resource
private RedisTemplate redisTemplate;
/**
* 关注/取关
*
* @param followDinerId 关注的食客ID
* @param isFollowed 是否关注 1=关注 0=取消
* @param accessToken 登录用户token
* @param path 访问地址
* @return
*/
@Transactional(rollbackFor = Exception.class)
public ResultInfo follow(Integer followDinerId, int isFollowed,
String accessToken, 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);
}
return ResultInfoUtil.build(ApiConstant.SUCCESS_CODE,
"关注成功", path, "关注成功");
}
// isValid值为0,那么是取关,如果isValid=1那么就是关注中
// 如果有关注信息,且目前处于取关状态,且要进行关注操作
if (follow != null && follow.getIsValid() == 0 && isFollowed == 1) {
// 重新关注
int count = followMapper.update(follow.getId(), isFollowed);
// 添加关注列表
if (count == 1) {
addToRedisSet(dinerInfo.getId(), followDinerId);
}
return ResultInfoUtil.build(ApiConstant.SUCCESS_CODE,
"关注成功", path, "关注成功");
}
// 如果有关注信息,且目前处于关注中状态,且要进行取关操作
if (follow != null && follow.getIsValid() == 1 && isFollowed == 0) {
// 取关
int count = followMapper.update(follow.getId(), isFollowed);
// 移除 Redis 关注列表
removeFromRedisSet(dinerInfo.getId(), followDinerId);
return ResultInfoUtil.build(ApiConstant.SUCCESS_CODE,
"成功取关", path, "成功取关");
}
return ResultInfoUtil.buildSuccess(path, "操作成功");
}
/**
* 添加关注列表到 Redis
*
* @param dinerId
* @param followDinerId
*/
private void addToRedisSet(Integer dinerId, Integer followDinerId) {
redisTemplate.opsForSet().add(RedisKeyConstant.following.getKey() + dinerId, followDinerId);
redisTemplate.opsForSet().add(RedisKeyConstant.followers.getKey() + followDinerId, dinerId);
}
/**
* 移除 Redis 关注列表
*
* @param dinerId
* @param followDinerId
*/
private void removeFromRedisSet(Integer dinerId, Integer followDinerId) {
redisTemplate.opsForSet().remove(RedisKeyConstant.following.getKey() + dinerId, followDinerId);
redisTemplate.opsForSet().remove(RedisKeyConstant.followers.getKey() + followDinerId, dinerId);
}
}
/**
* 共同好友
*
* @param dinerId
* @param accessToken
* @param path
* @return
*/
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));
if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
resultInfo.setPath(path);
return resultInfo;
}
// 处理结果集
List<LinkedHashMap> dinerInfoMaps = (ArrayList) resultInfo.getData();
List<ShortDinerInfo> dinerInfos = dinerInfoMaps.stream()
.map(diner -> fillBeanWithMap(diner, new ShortDinerInfo(), true))
.collect(Collectors.toList());
return ResultInfoUtil.buildSuccess(path, dinerInfos);
}