Springboot仿抖音app开发之粉丝业务模块后端复盘及相关业务知识总结
Springboot仿抖音app开发之用短视频务模块后端复盘及相关业务知识总结
Springboot仿抖音app开发之用户业务模块后端复盘及相关业务知识总结
Springboot仿抖音app开发之评论业务模块后端复盘及相关业务知识总结
消息数据存储入库选型
重要数据(如订单、价格、商户)选择MySQL的原因
事务支持:
- ACID特性:MySQL 支持事务处理,确保数据的一致性和完整性。这对于订单、价格等重要数据至关重要,因为这些数据的准确性直接影响到业务的正常运行和用户的信任。
- 回滚机制:在发生错误时,可以回滚事务,确保数据不会处于不一致状态。
数据一致性:
- 强一致性:关系型数据库通过外键约束、唯一性约束等机制,确保数据的一致性和完整性。
- 复杂查询:MySQL 支持复杂的 SQL 查询,可以轻松处理多表联查、聚合查询等复杂操作,这对于订单管理和报表生成非常有用。
成熟稳定:
- 广泛使用:MySQL 是一个成熟且广泛使用的数据库,有大量的社区支持和文档,易于维护和扩展。
- 安全性:MySQL 提供了多种安全机制,如用户权限管理、SSL 加密等,确保数据的安全性。
非重要数据(如日志、快照、消息)选择MongoDB的原因
高可扩展性:
- 水平扩展:MongoDB 支持水平扩展,可以通过分片(sharding)来处理大规模数据,适合处理日志、快照等大量非结构化数据。
- 分布式存储:MongoDB 可以轻松地在多台服务器上分布数据,提高系统的可用性和性能。
灵活的文档模型:
- 动态模式:MongoDB 使用 BSON(二进制 JSON)格式存储数据,支持灵活的文档模型,可以轻松处理结构化和非结构化数据。
- 嵌套数据:MongoDB 支持嵌套数据结构,适合存储日志、快照等复杂数据。
高性能:
- 读写性能:MongoDB 在处理大量读写操作时表现出色,适合日志记录、消息队列等高并发场景。
- 索引支持:MongoDB 支持多种索引类型,可以优化查询性能。
成本效益:
- 存储成本:MongoDB 可以更高效地存储大量非结构化数据,相对于关系型数据库,存储成本更低。
- 运维成本:MongoDB 的管理和维护相对简单,可以减少运维成本。
具体应用场景
订单数据:
- 重要性:订单数据直接影响到交易的完成和用户的信任,必须确保数据的一致性和完整性。
- 选择:MySQL
价格数据:
- 重要性:价格数据直接影响到商品的销售和用户的购买决策,必须确保数据的准确性和一致性。
- 选择:MySQL
商户数据:
- 重要性:商户数据涉及商家的注册信息、资质审核等,必须确保数据的安全性和一致性。
- 选择:MySQL
日志数据:
- 重要性:日志数据主要用于系统监控和问题排查,虽然重要但不需要强一致性。
- 选择:MongoDB
快照数据:
- 重要性:快照数据用于记录系统状态,主要用于审计和恢复,不需要强一致性。
- 选择:MongoDB
消息数据:
- 重要性:消息数据用于系统内部通信,需要高并发处理能力,但不需要强一致性。
- 选择:MongoDB
保存系统消息到MongoDB
1. 数据模型设计 (MessageMO)
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Document("message")
public class MessageMO {
@Id
private String id; // 消息主键id
@Field("fromUserId")
private String fromUserId; // 消息来自的用户id
@Field("fromNickname")
private String fromNickname; // 消息来自的用户昵称
@Field("fromFace")
private String fromFace; // 消息来自的用户头像
@Field("toUserId")
private String toUserId; // 消息发送到某对象的用户id
@Field("msgType")
private Integer msgType; // 消息类型 枚举
@Field("msgContent")
private Map msgContent; // 消息内容
@Field("createTime")
private Date createTime; // 消息创建时间
}
设计特点:
- 使用
@Document
注解指定MongoDB集合名为"message" - 采用
@Field
注解显式指定字段名,避免命名冲突 - 消息内容设计为Map类型,提供灵活的数据结构支持
- 使用Lombok简化代码,自动生成getter/setter和构造函数
2. 数据访问层 (MessageRepository)
@Repository
public interface MessageRepository extends MongoRepository<MessageMO, String> {
// 通过实现Repository,自定义条件查询
List<MessageMO> findAllByToUserIdEqualsOrderByCreateTimeDesc(String toUserId,
Pageable pageable);
// void deleteAllByFromUserIdAndToUserIdAndMsgType();
}
设计特点:
- 继承
MongoRepository
获取基本CRUD操作能力 - 使用Spring Data方法命名约定创建自定义查询
- 支持分页查询消息列表
- 注释掉的方法显示可能有删除特定消息的需求
3. 服务接口 (MsgService)
public interface MsgService {
/**
* 创建消息
*/
public void createMsg(String fromUserId,
String toUserId,
Integer type,
Map msgContent);
}
设计特点:
- 定义了创建消息的业务接口
- 支持指定发送者、接收者、消息类型和内容
- 接口简洁明了,职责单一
4. 服务实现 (MsgServiceImpl)
@Service
public class MsgServiceImpl extends BaseInfoProperties implements MsgService {
@Autowired
private MessageRepository messageRepository;
@Autowired
private UserService userService;
@Override
public void createMsg(String fromUserId,
String toUserId,
Integer type,
Map msgContent) {
Users fromUser = userService.getUser(fromUserId);
MessageMO messageMO = new MessageMO();
messageMO.setFromUserId(fromUserId);
messageMO.setFromNickname(fromUser.getNickname());
messageMO.setFromFace(fromUser.getFace());
messageMO.setToUserId(toUserId);
messageMO.setMsgType(type);
if (msgContent != null) {
messageMO.setMsgContent(msgContent);
}
messageMO.setCreateTime(new Date());
messageRepository.save(messageMO);
}
}
设计特点:
- 继承
BaseInfoProperties
获取通用属性和方法 - 注入
MessageRepository
进行数据操作 - 注入
UserService
获取用户信息 - 实现
createMsg
方法,完成消息创建和存储 - 自动生成创建时间,确保时间准确性
系统消息入库保存 - 关注
关注功能中的消息触发
// 系统消息:关注
msgService.createMsg(myId, vlogerId, MessageEnum.FOLLOW_YOU.type, null);
这行代码位于doFollow
方法中,当关注关系建立后被调用。参数解析:
myId
: 当前用户ID(关注者)vlogerId
: 被关注的用户ID(被关注者)MessageEnum.FOLLOW_YOU.type
: 消息类型常量,表示"关注你"的消息类型null
: 与消息相关的内容ID,关注消息不需要关联额外内容,因此为null
MsgService服务实现分析
虽然没有直接提供MsgService
的实现代码,但我们可以根据参数和命名推断其工作原理:
// MsgService接口中可能的方法定义
public void createMsg(String fromUserId, String toUserId, Integer msgType, String msgContent);
// MsgServiceImpl可能的实现
@Override
public void createMsg(String fromUserId, String toUserId, Integer msgType, String msgContent) {
// 1. 创建消息对象
String msgId = sid.nextShort(); // 生成ID
MessageMO messageMO = new MessageMO();
messageMO.setId(msgId);
messageMO.setFromUserId(fromUserId);
messageMO.setToUserId(toUserId);
messageMO.setMsgType(msgType);
messageMO.setMsgContent(msgContent);
messageMO.setCreateTime(new Date());
// 2. 保存到数据库
messageMapper.insert(messageMO);
// 3. 可能的推送逻辑
// 如果需要实时通知,这里可能会调用推送服务
}
消息类型枚举设计
代码中使用了MessageEnum.FOLLOW_YOU.type
,这表明系统采用了枚举来定义不同类型的消息:
public enum MessageEnum {
FOLLOW_YOU(1, "关注"),
LIKE_VLOG(2, "点赞视频"),
COMMENT_VLOG(3, "评论视频"),
REPLY_YOU(4, "回复评论"),
LIKE_COMMENT(5, "点赞评论");
public final Integer type;
public final String value;
MessageEnum(Integer type, String value) {
this.type = type;
this.value = value;
}
}
系统消息入库保存 - 点赞短视频
1. 记录点赞关系
首先,系统会在数据库中记录用户点赞视频的关系:
String rid = sid.nextShort();
MyLikedVlog likedVlog = new MyLikedVlog();
likedVlog.setId(rid);
likedVlog.setVlogId(vlogId);
likedVlog.setUserId(userId);
myLikedVlogMapper.insert(likedVlog);
这一步确保了用户的点赞行为被持久化,为后续的业务逻辑提供数据基础。
2. 获取视频详情
为了构建有意义的消息内容,系统需要获取被点赞视频的详细信息:
Vlog vlog = this.getVlog(vlogId);
getVlog
方法简单封装了对数据库的查询操作:
@Override
public Vlog getVlog(String id) {
return vlogMapper.selectByPrimaryKey(id);
}
3. 构建消息内容
与"关注"消息不同,"点赞视频"消息需要包含更多上下文信息,因此构建了一个Map作为消息内容:
Map msgContent = new HashMap();
msgContent.put("vlogId", vlogId);
msgContent.put("vlogCover", vlog.getCover());
这个消息内容包含了:
vlogId
: 被点赞的视频ID,便于接收消息后跳转vlogCover
: 视频封面图,用于在消息中展示缩略图
4. 创建系统消息
最后,调用消息服务创建系统消息:
msgService.createMsg(userId,
vlog.getVlogerId(),
MessageEnum.LIKE_VLOG.type,
msgContent);
参数解析:
userId
: 点赞者的用户ID(消息发送者)vlog.getVlogerId()
: 视频创作者的用户ID(消息接收者)MessageEnum.LIKE_VLOG.type
: 消息类型为"点赞视频"msgContent
: 包含视频ID和封面的Map对象
系统消息入库保存 - 评论与回复
我们在commentserviceImpl中修改方法 我们先看完整代码
@Override
public CommentVO createComment(CommentBO commentBO) {
String commentId = sid.nextShort();
Comment comment = new Comment();
comment.setId(commentId);
comment.setVlogId(commentBO.getVlogId());
comment.setVlogerId(commentBO.getVlogerId());
comment.setCommentUserId(commentBO.getCommentUserId());
comment.setFatherCommentId(commentBO.getFatherCommentId());
comment.setContent(commentBO.getContent());
comment.setLikeCounts(0);
comment.setCreateTime(new Date());
commentMapper.insert(comment);
// redis操作放在service中,评论总数的累加
redis.increment(REDIS_VLOG_COMMENT_COUNTS + ":" + commentBO.getVlogId(), 1);
// 留言后的最新评论需要返回给前端进行展示
CommentVO commentVO = new CommentVO();
BeanUtils.copyProperties(comment, commentVO);
// 系统消息:评论/回复
Vlog vlog = vlogService.getVlog(commentBO.getVlogId());
Map msgContent = new HashMap();
msgContent.put("vlogId", vlog.getId());
msgContent.put("vlogCover", vlog.getCover());
msgContent.put("commentId", commentId);
msgContent.put("commentContent", commentBO.getContent());
Integer type = MessageEnum.COMMENT_VLOG.type;
if (StringUtils.isNotBlank(commentBO.getFatherCommentId()) &&
!commentBO.getFatherCommentId().equalsIgnoreCase("0") ) {
type = MessageEnum.REPLY_YOU.type;
}
msgService.createMsg(commentBO.getCommentUserId(),
commentBO.getVlogerId(),
type,
msgContent);
return commentVO;
}
消息创建流程分析
在createComment
方法中,评论/回复信息入库后,系统会执行以下步骤创建系统消息:
1. 获取视频信息
Vlog vlog = vlogService.getVlog(commentBO.getVlogId());
首先获取被评论视频的详细信息,用于构建消息内容。
2. 构建消息内容
Map msgContent = new HashMap();
msgContent.put("vlogId", vlog.getId());
msgContent.put("vlogCover", vlog.getCover());
msgContent.put("commentId", commentId);
msgContent.put("commentContent", commentBO.getContent());
与点赞视频消息相比,评论/回复消息包含更丰富的内容:
vlogId
: 视频IDvlogCover
: 视频封面commentId
: 评论ID(新创建的)commentContent
: 评论内容
这些信息使接收者能够直接查看评论内容,并提供了上下文参考。
3. 确定消息类型
Integer type = MessageEnum.COMMENT_VLOG.type;
if (StringUtils.isNotBlank(commentBO.getFatherCommentId()) &&
!commentBO.getFatherCommentId().equalsIgnoreCase("0")) {
type = MessageEnum.REPLY_YOU.type;
}
这段代码通过检查fatherCommentId
(父评论ID)来区分直接评论和回复:
- 如果
fatherCommentId
为空或为"0",表示这是对视频的直接评论,消息类型为COMMENT_VLOG
- 否则,表示这是对已有评论的回复,消息类型为
REPLY_YOU
4. 创建系统消息
msgService.createMsg(commentBO.getCommentUserId(),
commentBO.getVlogerId(),
type,
msgContent);
最后调用消息服务创建系统消息,参数包括:
- 发送者ID:评论用户ID
- 接收者ID:视频创作者ID
- 消息类型:评论或回复
- 消息内容:包含视频和评论信息的Map
系统消息入库保存 - 点赞评论
消息创建流程
当用户点赞一条评论时,系统会执行以下步骤来创建系统消息:
1. 获取评论信息
Comment comment = commentService.getComment(commentId);
首先获取被点赞评论的详细信息,包括关联的视频ID和评论作者ID。
2. 获取视频信息
Vlog vlog = vlogService.getVlog(comment.getVlogId());
通过评论中的视频ID,获取视频的详细信息,用于构建消息内容。
3. 构建消息内容
Map msgContent = new HashMap();
msgContent.put("vlogId", vlog.getId());
msgContent.put("vlogCover", vlog.getCover());
msgContent.put("commentId", commentId);
构建包含三个关键信息的消息内容:
vlogId
: 视频ID,用于定位评论所属的视频vlogCover
: 视频封面,用于在消息中展示视觉元素commentId
: 评论ID,用于定位具体的评论
4. 创建系统消息
msgService.createMsg(userId,
comment.getCommentUserId(),
MessageEnum.LIKE_COMMENT.type,
msgContent);
最后调用消息服务创建系统消息,参数包括:
- 发送者ID:点赞用户的ID
- 接收者ID:评论作者的ID(注意不是视频作者)
- 消息类型:点赞评论(LIKE_COMMENT)
- 消息内容:包含视频和评论信息的Map
MongoDB分页查询系统消息列表
控制器层实现 (Controller)
@Slf4j
@Api(tags = "MsgController 消息功能模块的接口")
@RequestMapping("msg")
@RestController
public class MsgController extends BaseInfoProperties {
@Autowired
private MsgService msgService;
@GetMapping("list")
public GraceJSONResult list(@RequestParam String userId,
@RequestParam Integer page,
@RequestParam Integer pageSize) {
// mongodb 从0分页,区别于数据库
if (page == null) {
page = COMMON_START_PAGE_ZERO;
}
if (pageSize == null) {
pageSize = COMMON_PAGE_SIZE;
}
List<MessageMO> list = msgService.queryList(userId, page, pageSize);
return GraceJSONResult.ok(list);
}
}
控制器层关键点:
- 提供RESTful API接口,映射到
/msg/list
路径 - 接收三个请求参数:用户ID、页码和每页大小
- 对页码和页大小进行默认值处理
- 调用服务层方法获取消息列表
- 返回统一格式的响应结果
服务层实现 (Service)
服务层实现了从数据访问层获取数据并进行业务处理的核心逻辑:
@Override
public List<MessageMO> queryList(String toUserId, Integer page, Integer pageSize) {
// 1. 创建分页请求对象
Pageable pageable = PageRequest.of(page,
pageSize,
Sort.Direction.DESC,
"createTime");
// 2. 调用Repository层方法执行MongoDB查询
List<MessageMO> list = messageRepository
.findAllByToUserIdEqualsOrderByCreateTimeDesc(toUserId, pageable);
// 3. 对查询结果进行后处理
for (MessageMO msg : list) {
// 如果类型是关注消息,则需要查询我之前有没有关注过他,用于在前端标记"互粉""互关"
if (msg.getMsgType() != null && msg.getMsgType() == MessageEnum.FOLLOW_YOU.type) {
Map map = msg.getMsgContent();
if (map == null) {
map = new HashMap();
}
String relationship = redis.get(REDIS_FANS_AND_VLOGGER_RELATIONSHIP + ":" + msg.getToUserId() + ":" + msg.getFromUserId());
if (StringUtils.isNotBlank(relationship) && relationship.equalsIgnoreCase("1")) {
map.put("isFriend", true);
} else {
map.put("isFriend", false);
}
msg.setMsgContent(map);
}
}
// 4. 返回处理后的消息列表
return list;
}
服务层关键实现步骤
创建分页请求对象 (Pageable)
PageRequest.of()
方法创建一个Pageable
实例- 参数说明:
page
: 页码索引(MongoDB从0开始计数)pageSize
: 每页记录数Sort.Direction.DESC
: 降序排序"createTime"
: 排序字段
执行MongoDB查询
- 调用
messageRepository
的方法 - 传入接收者ID和分页对象
- 返回符合条件的消息列表
- 调用
消息后处理
- 遍历查询结果
- 针对关注类型消息进行特殊处理
- 查询Redis获取关注关系
- 向消息内容中添加互粉标记
状态判断逻辑
- 从Redis获取键值:
REDIS_FANS_AND_VLOGGER_RELATIONSHIP:接收者ID:发送者ID
- 值为"1"表示双向关注
- 设置
isFriend
标记,用于前端显示"互关"状态
- 从Redis获取键值:
数据访问层实现 (Repository)
数据访问层通过Spring Data MongoDB提供的方法命名约定来定义查询方法:
// 通过实现Repository,自定义条件查询
List<MessageMO> findAllByToUserIdEqualsOrderByCreateTimeDesc(String toUserId,
Pageable pageable);
数据访问层工作原理
Repository接口定义
- 通常继承自
MongoRepository<MessageMO, String>
- 无需编写实现类,Spring Data会自动提供实现
- 通常继承自
方法命名约定
findAllByToUserIdEquals
: 查找所有接收者ID等于指定值的消息OrderByCreateTimeDesc
: 按创建时间降序排序
方法参数
String toUserId
: 消息接收者IDPageable pageable
: 分页和排序参数
MongoDB查询转换
- Spring Data将方法名解析为MongoDB查询
- 生成类似于
db.messages.find({toUserId: ?}).sort({createTime: -1}).skip(?).limit(?)
Spring Data自定义查询方法详解
方法签名分析
List<MessageMO> findAllByToUserIdEqualsOrderByCreateTimeDesc(String toUserId, Pageable pageable);
这是一个在Repository接口中定义的方法,Spring Data会根据方法名自动生成查询实现。让我们逐部分分析:
1. 返回类型
List<MessageMO>
- 返回一个
MessageMO
对象的集合 MessageMO
应该是消息的MongoDB文档对象(Message MongoDB Object)- 表明这是一个可能返回多条记录的查询
2. 方法名称解析
方法名可以分解为几个部分,Spring Data根据这些部分自动构建查询:
findAll
: 查询操作,表示获取所有匹配的记录ByToUserIdEquals
: 查询条件,表示字段toUserId
必须等于提供的参数OrderByCreateTimeDesc
: 排序条件,表示结果按createTime
字段降序排列
3. 参数列表
(String toUserId, Pageable pageable)
toUserId
: 用于匹配消息接收者的IDPageable pageable
: Spring Data提供的分页和排序参数对象
实际执行的查询
这个方法会被Spring Data转换为类似以下的MongoDB查询:
db.messages.find({ toUserId: "用户ID值" })
.sort({ createTime: -1 })
.skip(pageable.getPageNumber() * pageable.getPageSize())
.limit(pageable.getPageSize())
或者如果是JPA/SQL,会转换为类似:
SELECT * FROM message
WHERE to_user_id = ?
ORDER BY create_time DESC
LIMIT ? OFFSET ?