个人项目:社交支付项目(小老板)
作者:三哥,j3code.cn
项目文档:www.yuque.com/g/j3code/dv...
预览地址(未开发完):admire.j3code.cn/small-boss
- 内网穿透部署,第一次访问比较慢
1、分析
现在的社交类项目中用户关注功能是必不可少的,该功能能让用户更聚焦于自己中意的那部分内容。就像 B 站关注功能,在你浏览视频时,推送的内容中有相当一部分内容都在自你关注的 UP。当然,我们肯定做不了 B 站那样的效果拉,但是这个关注功能我们还是可以拎出来,实现实现。
就像这样:

这个图片,我们可以得出三个信息:
- 关注按钮
- 关注数据项
- 粉丝数据项
理解起来也不是很难,关注按钮就是我们提供的关注功能,点击就当前登录用户关注该页面用户;关注数据项表示该页面用户关注了那些用户;粉丝数据项就是有多少个用户关注了该页面用户。
大致了解了这三项信息之后,再来看看用户 A 关注用户 B,应该需要记录那些字段。
首先肯定是需要记录这个动作的发起方用户 ID,对吧!咱们把他定义为 user_id。接着我们是不是要记录一下这个动作的接收方,也即被关注者的用户ID,咱们把他定义为 follow_user_id。如果我在加一个 status 字段记录用户关注或者取消关注,是否可行?
可能有人会说,麻烦,多记录这一个干啥!关注就插入一条数据,取消关注就删除这条数据,不就 ok 了。
其实数据的删除和修改本身没太大区别(效率上也没啥区别),而且你不觉得删除的危险性会比修改搞嘛!万一你脑子一热删除数据的时候不带条件,那数据是不是就清空了。而修改的话则数据都还在,只是出现了数据的混乱。对于用户来说有数据总比没有数据强,对吧!
所以这里我就建议关注和取消都修改 status 字段来实现用户的关注功能/取消功能。
基本的字段我们都确定好了,再来看看后面的数据项场景,也即如果我根据这些字段,如何查找一个用户的关注数据?
关注数据查询:
查询一个用户的关注数据,我们只需要根据用户的 id,去 MySQL 中找到 user_id 是传入的 id,且 status 为关注的数据即可,伪 SQL 如下:
select * from 表 where user_id = 用户id and status = 关注
再来看看用户的粉丝数据如何查找
粉丝数据查询:
查询一个用户的粉丝数据,我们只需要根据用户的 id,去 MySQL 中找到 follow_user_id 是传入的 id,且 status 为 关注的数据即可,伪 SQL 如下:
select * from 表 where follow_user_id = 用户id and status = 关注
这样看似非常好,一个关注功能就做好了,如果你们是这么想的话,我只能说 dome 罢了。
2、再分析
按照上面的分析,如果有下面两个场景:
- 用户 A 关注用户 B
- 用户 B 关注用户 A
是不是会产生两条关注数据,只不过是 user_id 与 follow_user_id 字段的内容对调了一下而已,所以我们能不能考虑将两者合并一下呢!
网上的大部分解决办法是加个互关字段:mutual_follow(0不互关,1互关)。
那,此时你想一下 status 这个字段的值是不是就不单单是关注/非关注这么简单了,对吧!是不是应该有如下状态:
- 两者一个都不关注,暂且赋值为 1
- user_id 不关注 follow_user_id,但此时 follow_user_id 任然关注 user_id,暂且赋值为 2
- follow_user_id 不关注 user_id,但此时 user_id 任然关注 follow_user_id,暂且赋值为 3
现在,按照上面的字段及其含义,来看看如何实现 A、B两个用户的数据查询:
关注数:
这里我直接写伪 SQL:
select * from 表 where user_id = 用户id and (status = 3 or mutual_follow = 1)
union all
select * from 表 where follow_user_id = 用户id and (status = 2 or mutual_follow = 1)
粉丝数:
这里我直接写伪 SQL:
select * from 表 where follow_user_id = 用户id and (status = 3 or mutual_follow = 1)
union all
select * from 表 where user_id= 用户id and (status = 2 or mutual_follow = 1)
看着上面的 SQL 是不是感觉头大,为什么会有这么烧脑的逻辑,反正我要是看着这段 SQL 绝对是要该掉的。因为为了减少数据的插入,而在一行数据中加入这么多的逻辑,我觉得反而不值得。再说了,当系统用户量上来之后,一张表肯定是不可能满足系统要求的,对吧!所以还要考虑分表。
那,如果还按照上面的分析,来进行分表,是不是又陷入困境了,肯本找不到合适的分片字段进行水平分表,所以最终的结果是,这个方案不行。
3、最终分析
我们能不能思路打开,不要只局限一个表,将关注和粉丝两种不同的数据进行分开存储,这不就行了嘛!
用户 A 关注了用户 B,那就再关注表中存入 A 关注 B 的关注数据,在粉丝表中存入 A 是 B 的粉丝数据这不就 ok 了嘛!后期分表也是非常好做的,直接对关注表和粉丝表进行分表,简单而容易理解。
那先来看看关注表字段:
- user_id:用户id
- follow_user_id:被关注用户id
- status:关注状态:1 关注,0 未关注
再来看看粉丝表字段
- user_id:用户id
- follower_id:粉丝id
- status:关注状态:1 关注,0 未关注
此时,如果 A 关注 B 了,表数据的存储如下:
关注表
user_id | follow_user_id | status |
---|---|---|
A | B | 1 |
粉丝表
user_id | follower_id | status |
---|---|---|
B | A | 1 |
怎么样,逻辑是不是非常清晰,后期要进行分表的话,也是非常好处理的,直接针对这两张表的 user_id 进行水平分表(因为所有的查都是通过 user_id 来的)。至于查询 SQL 我相信学过 MySQL 的都会写,我就不在这里赘述了。
不过,这里也是有点问题,就是在插入数据和修改数据的时候,我们要同时操作两张表这就要保证整个操作的原子性了,不过这个我觉得还是很好处理的,后面看我代码实现(其实就是加事务)。
最后再来个图结束本次的分析,如下:

4、实现
先来创建一下两张表,SQL 如下:
关注表:
sql
CREATE TABLE `sb_user_follow` (
`id` bigint(20) NOT NULL,
`user_id` bigint(20) NOT NULL COMMENT '用户id',
`follow_user_id` bigint(20) NOT NULL COMMENT '关注的用户id',
`status` tinyint(1) DEFAULT '1' COMMENT '状态,false未关注,true关注',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `un` (`user_id`,`follow_user_id`),
KEY `key` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci
粉丝表:
sql
CREATE TABLE `sb_user_follower` (
`id` bigint(20) NOT NULL,
`user_id` bigint(20) NOT NULL COMMENT '用户id',
`follower_id` bigint(20) NOT NULL COMMENT '粉丝id',
`status` tinyint(1) DEFAULT '1' COMMENT '状态,false未关注,true关注',
PRIMARY KEY (`id`),
UNIQUE KEY `un` (`user_id`,`follower_id`),
KEY `key` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci
如果系统中本身就就引入了分表组件,我建议直接对这两张表进行分表,省的后期又要过来做分表操作,这里索性一步到位。
注:MyBatisX 插件自动生成代码我就不过多赘述,已经讲过很多遍了。
所有前期准备做好只好,来看看关注功能的实现:
1)controller
位置:cn.j3code.community.api.v1.controller
java
@Slf4j
@ResponseResult
@AllArgsConstructor
@RestController
@RequestMapping(UrlPrefixConstants.WEB_V1 + "/userFollow")
public class UserFollowController {
private final UserFollowService userFollowService;
/**
* 关注
*
* @param userId 用户id
* @param status 状态
*/
@GetMapping("/follow")
public void follow(@RequestParam("userId") Long userId, @RequestParam("status") FollowStatusEnum status) {
userFollowService.follow(userId, status);
}
}
FollowStatusEnum 枚举对象
位置:cn.j3code.config.enums
java
@Getter
public enum FollowStatusEnum {
FOLLOW(true, "关注"),
NOT_FOLLOW(false, "取消关注"),
;
@EnumValue
private Boolean value;
private String description;
FollowStatusEnum(Boolean value, String description) {
this.value = value;
this.description = description;
}
}
2)service
位置:cn.j3code.community.service
java
public interface UserFollowService extends IService<UserFollow> {
void follow(Long userId, FollowStatusEnum status);
}
@Slf4j
@AllArgsConstructor
@Service
public class UserFollowServiceImpl extends ServiceImpl<UserFollowMapper, UserFollow>
implements UserFollowService {
private final UserFollowerService userFollowerService;
private final TransactionTemplate transactionTemplate;
@Override
public void follow(Long userId, FollowStatusEnum status) {
if (userId.equals(SecurityUtil.getUserId())) {
throw new SysException("真坏,自己还要关注嘛!");
}
// 关注对象
UserFollow follow = new UserFollow();
follow.setStatus(status.getValue());
// 我关注了 userId
follow.setUserId(SecurityUtil.getUserId());
follow.setFollowUserId(userId);
follow.setCreateTime(LocalDateTime.now());
follow.setUpdateTime(LocalDateTime.now());
// 粉丝对象
UserFollower follower = new UserFollower();
follower.setStatus(status.getValue());
// 我是 userId 的粉丝
follower.setFollowerId(SecurityUtil.getUserId());
follower.setUserId(userId);
MyTransactionTemplate.execute(transactionTemplate, accept -> {
// 保存关注对象
getBaseMapper().follow(follow);
// 保存粉丝对象
userFollowerService.follower(follower);
}, status.getDescription() + "失败,稍后重试!");
if (FollowStatusEnum.NOT_FOLLOW.equals(status)) {
return;
}
// 消息通知,对关注动作
}
}
3)mapper
UserFollowMapper 实现:
xml
<insert id="follow">
insert into sb_user_follow(user_id,follow_user_id,`status`,create_time,update_time)
values
(
#{follow.userId},
#{follow.followUserId},
#{follow.status},
#{follow.createTime},
#{follow.updateTime}
)
on duplicate key update
`status` = VALUES(`status`),
`update_time` = VALUES(`update_time`)
</insert>
UserFollowerMapper 实现:
xml
<insert id="follower">
insert into sb_user_follower(user_id,follower_id,`status`)
values
(
#{follower.userId},
#{follower.followerId},
#{follower.status}
)
on duplicate key update
`status` = VALUES(`status`)
</insert>
到此,我们的关注功能算是完结了,至于后期的关注列表数据和粉丝列表数据就非常简单,我相信大家应该可以动手查出来,我就不贴代码了。