Springboot仿抖音app开发之消息业务模块后端复盘及相关业务知识总结

Springboot仿抖音app开发之粉丝业务模块后端复盘及相关业务知识总结

Springboot仿抖音app开发之用短视频务模块后端复盘及相关业务知识总结

Springboot仿抖音app开发之用户业务模块后端复盘及相关业务知识总结

Springboot仿抖音app开发之评论业务模块后端复盘及相关业务知识总结

消息数据存储入库选型

重要数据(如订单、价格、商户)选择MySQL的原因

  1. 事务支持

    • ACID特性:MySQL 支持事务处理,确保数据的一致性和完整性。这对于订单、价格等重要数据至关重要,因为这些数据的准确性直接影响到业务的正常运行和用户的信任。
    • 回滚机制:在发生错误时,可以回滚事务,确保数据不会处于不一致状态。
  2. 数据一致性

    • 强一致性:关系型数据库通过外键约束、唯一性约束等机制,确保数据的一致性和完整性。
    • 复杂查询:MySQL 支持复杂的 SQL 查询,可以轻松处理多表联查、聚合查询等复杂操作,这对于订单管理和报表生成非常有用。
  3. 成熟稳定

    • 广泛使用:MySQL 是一个成熟且广泛使用的数据库,有大量的社区支持和文档,易于维护和扩展。
    • 安全性:MySQL 提供了多种安全机制,如用户权限管理、SSL 加密等,确保数据的安全性。

非重要数据(如日志、快照、消息)选择MongoDB的原因

  1. 高可扩展性

    • 水平扩展:MongoDB 支持水平扩展,可以通过分片(sharding)来处理大规模数据,适合处理日志、快照等大量非结构化数据。
    • 分布式存储:MongoDB 可以轻松地在多台服务器上分布数据,提高系统的可用性和性能。
  2. 灵活的文档模型

    • 动态模式:MongoDB 使用 BSON(二进制 JSON)格式存储数据,支持灵活的文档模型,可以轻松处理结构化和非结构化数据。
    • 嵌套数据:MongoDB 支持嵌套数据结构,适合存储日志、快照等复杂数据。
  3. 高性能

    • 读写性能:MongoDB 在处理大量读写操作时表现出色,适合日志记录、消息队列等高并发场景。
    • 索引支持:MongoDB 支持多种索引类型,可以优化查询性能。
  4. 成本效益

    • 存储成本:MongoDB 可以更高效地存储大量非结构化数据,相对于关系型数据库,存储成本更低。
    • 运维成本:MongoDB 的管理和维护相对简单,可以减少运维成本。

具体应用场景

  1. 订单数据

    • 重要性:订单数据直接影响到交易的完成和用户的信任,必须确保数据的一致性和完整性。
    • 选择:MySQL
  2. 价格数据

    • 重要性:价格数据直接影响到商品的销售和用户的购买决策,必须确保数据的准确性和一致性。
    • 选择:MySQL
  3. 商户数据

    • 重要性:商户数据涉及商家的注册信息、资质审核等,必须确保数据的安全性和一致性。
    • 选择:MySQL
  4. 日志数据

    • 重要性:日志数据主要用于系统监控和问题排查,虽然重要但不需要强一致性。
    • 选择:MongoDB
  5. 快照数据

    • 重要性:快照数据用于记录系统状态,主要用于审计和恢复,不需要强一致性。
    • 选择:MongoDB
  6. 消息数据

    • 重要性:消息数据用于系统内部通信,需要高并发处理能力,但不需要强一致性。
    • 选择:MongoDB

保存系统消息到MongoDB

1. 数据模型设计 (MessageMO)

java 复制代码
@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)

java 复制代码
@Repository
public interface MessageRepository extends MongoRepository<MessageMO, String> {
    // 通过实现Repository,自定义条件查询
    List<MessageMO> findAllByToUserIdEqualsOrderByCreateTimeDesc(String toUserId,
                                                              Pageable pageable);
    //    void deleteAllByFromUserIdAndToUserIdAndMsgType();
}

设计特点

  • 继承MongoRepository获取基本CRUD操作能力
  • 使用Spring Data方法命名约定创建自定义查询
  • 支持分页查询消息列表
  • 注释掉的方法显示可能有删除特定消息的需求

3. 服务接口 (MsgService)

java 复制代码
public interface MsgService {
    /**
     * 创建消息
     */
    public void createMsg(String fromUserId,
                         String toUserId,
                         Integer type,
                         Map msgContent);
}

设计特点

  • 定义了创建消息的业务接口
  • 支持指定发送者、接收者、消息类型和内容
  • 接口简洁明了,职责单一

4. 服务实现 (MsgServiceImpl)

java 复制代码
@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方法,完成消息创建和存储
  • 自动生成创建时间,确保时间准确性

系统消息入库保存 - 关注

关注功能中的消息触发

java 复制代码
// 系统消息:关注
msgService.createMsg(myId, vlogerId, MessageEnum.FOLLOW_YOU.type, null);

这行代码位于doFollow方法中,当关注关系建立后被调用。参数解析:

  • myId: 当前用户ID(关注者)
  • vlogerId: 被关注的用户ID(被关注者)
  • MessageEnum.FOLLOW_YOU.type: 消息类型常量,表示"关注你"的消息类型
  • null: 与消息相关的内容ID,关注消息不需要关联额外内容,因此为null

MsgService服务实现分析

虽然没有直接提供MsgService的实现代码,但我们可以根据参数和命名推断其工作原理:

java 复制代码
// MsgService接口中可能的方法定义
public void createMsg(String fromUserId, String toUserId, Integer msgType, String msgContent);
java 复制代码
// 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,这表明系统采用了枚举来定义不同类型的消息:

java 复制代码
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. 记录点赞关系

首先,系统会在数据库中记录用户点赞视频的关系:

java 复制代码
String rid = sid.nextShort();
MyLikedVlog likedVlog = new MyLikedVlog();
likedVlog.setId(rid);
likedVlog.setVlogId(vlogId);
likedVlog.setUserId(userId);
myLikedVlogMapper.insert(likedVlog);

这一步确保了用户的点赞行为被持久化,为后续的业务逻辑提供数据基础。

2. 获取视频详情

为了构建有意义的消息内容,系统需要获取被点赞视频的详细信息:

java 复制代码
Vlog vlog = this.getVlog(vlogId);

getVlog方法简单封装了对数据库的查询操作:

java 复制代码
@Override
public Vlog getVlog(String id) {
    return vlogMapper.selectByPrimaryKey(id);
}

3. 构建消息内容

与"关注"消息不同,"点赞视频"消息需要包含更多上下文信息,因此构建了一个Map作为消息内容:

java 复制代码
Map msgContent = new HashMap();
msgContent.put("vlogId", vlogId);
msgContent.put("vlogCover", vlog.getCover());

这个消息内容包含了:

  • vlogId: 被点赞的视频ID,便于接收消息后跳转
  • vlogCover: 视频封面图,用于在消息中展示缩略图

4. 创建系统消息

最后,调用消息服务创建系统消息:

java 复制代码
msgService.createMsg(userId,
                    vlog.getVlogerId(),
                    MessageEnum.LIKE_VLOG.type,
                    msgContent);

参数解析:

  • userId: 点赞者的用户ID(消息发送者)
  • vlog.getVlogerId(): 视频创作者的用户ID(消息接收者)
  • MessageEnum.LIKE_VLOG.type: 消息类型为"点赞视频"
  • msgContent: 包含视频ID和封面的Map对象

系统消息入库保存 - 评论与回复

我们在commentserviceImpl中修改方法 我们先看完整代码

java 复制代码
@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. 获取视频信息

java 复制代码
Vlog vlog = vlogService.getVlog(commentBO.getVlogId());

首先获取被评论视频的详细信息,用于构建消息内容。

2. 构建消息内容

java 复制代码
Map msgContent = new HashMap();
msgContent.put("vlogId", vlog.getId());
msgContent.put("vlogCover", vlog.getCover());
msgContent.put("commentId", commentId);
msgContent.put("commentContent", commentBO.getContent());

与点赞视频消息相比,评论/回复消息包含更丰富的内容:

  • vlogId: 视频ID
  • vlogCover: 视频封面
  • commentId: 评论ID(新创建的)
  • commentContent: 评论内容

这些信息使接收者能够直接查看评论内容,并提供了上下文参考。

3. 确定消息类型

java 复制代码
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. 创建系统消息

java 复制代码
msgService.createMsg(commentBO.getCommentUserId(),
                    commentBO.getVlogerId(),
                    type,
                    msgContent);

最后调用消息服务创建系统消息,参数包括:

  • 发送者ID:评论用户ID
  • 接收者ID:视频创作者ID
  • 消息类型:评论或回复
  • 消息内容:包含视频和评论信息的Map

系统消息入库保存 - 点赞评论

消息创建流程

当用户点赞一条评论时,系统会执行以下步骤来创建系统消息:

1. 获取评论信息

java 复制代码
Comment comment = commentService.getComment(commentId);

首先获取被点赞评论的详细信息,包括关联的视频ID和评论作者ID。

2. 获取视频信息

java 复制代码
Vlog vlog = vlogService.getVlog(comment.getVlogId());

通过评论中的视频ID,获取视频的详细信息,用于构建消息内容。

3. 构建消息内容

java 复制代码
Map msgContent = new HashMap();
msgContent.put("vlogId", vlog.getId());
msgContent.put("vlogCover", vlog.getCover());
msgContent.put("commentId", commentId);

构建包含三个关键信息的消息内容:

  • vlogId: 视频ID,用于定位评论所属的视频
  • vlogCover: 视频封面,用于在消息中展示视觉元素
  • commentId: 评论ID,用于定位具体的评论

4. 创建系统消息

java 复制代码
msgService.createMsg(userId,
                   comment.getCommentUserId(),
                   MessageEnum.LIKE_COMMENT.type,
                   msgContent);

最后调用消息服务创建系统消息,参数包括:

  • 发送者ID:点赞用户的ID
  • 接收者ID:评论作者的ID(注意不是视频作者)
  • 消息类型:点赞评论(LIKE_COMMENT)
  • 消息内容:包含视频和评论信息的Map

MongoDB分页查询系统消息列表

控制器层实现 (Controller)

java 复制代码
@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)

服务层实现了从数据访问层获取数据并进行业务处理的核心逻辑:

java 复制代码
@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;
}

服务层关键实现步骤

  1. 创建分页请求对象 (Pageable)

    • PageRequest.of() 方法创建一个 Pageable 实例
    • 参数说明:
      • page: 页码索引(MongoDB从0开始计数)
      • pageSize: 每页记录数
      • Sort.Direction.DESC: 降序排序
      • "createTime": 排序字段
  2. 执行MongoDB查询

    • 调用 messageRepository 的方法
    • 传入接收者ID和分页对象
    • 返回符合条件的消息列表
  3. 消息后处理

    • 遍历查询结果
    • 针对关注类型消息进行特殊处理
    • 查询Redis获取关注关系
    • 向消息内容中添加互粉标记
  4. 状态判断逻辑

    • 从Redis获取键值:REDIS_FANS_AND_VLOGGER_RELATIONSHIP:接收者ID:发送者ID
    • 值为"1"表示双向关注
    • 设置isFriend标记,用于前端显示"互关"状态

数据访问层实现 (Repository)

数据访问层通过Spring Data MongoDB提供的方法命名约定来定义查询方法:

java 复制代码
// 通过实现Repository,自定义条件查询
List<MessageMO> findAllByToUserIdEqualsOrderByCreateTimeDesc(String toUserId, 
                                                          Pageable pageable);

数据访问层工作原理

  1. Repository接口定义

    • 通常继承自 MongoRepository<MessageMO, String>
    • 无需编写实现类,Spring Data会自动提供实现
  2. 方法命名约定

    • findAllByToUserIdEquals: 查找所有接收者ID等于指定值的消息
    • OrderByCreateTimeDesc: 按创建时间降序排序
  3. 方法参数

    • String toUserId: 消息接收者ID
    • Pageable pageable: 分页和排序参数
  4. MongoDB查询转换

    • Spring Data将方法名解析为MongoDB查询
    • 生成类似于 db.messages.find({toUserId: ?}).sort({createTime: -1}).skip(?).limit(?)

Spring Data自定义查询方法详解

方法签名分析

java 复制代码
List<MessageMO> findAllByToUserIdEqualsOrderByCreateTimeDesc(String toUserId, Pageable pageable);

这是一个在Repository接口中定义的方法,Spring Data会根据方法名自动生成查询实现。让我们逐部分分析:

1. 返回类型

java 复制代码
List<MessageMO>
  • 返回一个MessageMO对象的集合
  • MessageMO应该是消息的MongoDB文档对象(Message MongoDB Object)
  • 表明这是一个可能返回多条记录的查询

2. 方法名称解析

方法名可以分解为几个部分,Spring Data根据这些部分自动构建查询:

  • findAll: 查询操作,表示获取所有匹配的记录
  • ByToUserIdEquals: 查询条件,表示字段toUserId必须等于提供的参数
  • OrderByCreateTimeDesc: 排序条件,表示结果按createTime字段降序排列

3. 参数列表

java 复制代码
(String toUserId, Pageable pageable)
  • toUserId: 用于匹配消息接收者的ID
  • Pageable pageable: Spring Data提供的分页和排序参数对象

实际执行的查询

这个方法会被Spring Data转换为类似以下的MongoDB查询:

javascript 复制代码
db.messages.find({ toUserId: "用户ID值" })
           .sort({ createTime: -1 })
           .skip(pageable.getPageNumber() * pageable.getPageSize())
           .limit(pageable.getPageSize())

或者如果是JPA/SQL,会转换为类似:

sql 复制代码
SELECT * FROM message 
WHERE to_user_id = ?
ORDER BY create_time DESC
LIMIT ? OFFSET ?
相关推荐
用户298698530148 分钟前
.NET 文档自动化:Spire.Doc 设置奇偶页页眉/页脚的最佳实践
后端·c#·.net
随风飘的云30 分钟前
mysql的innodb引擎对可重复读做了那些优化,可以避免幻读
mysql
码路飞36 分钟前
GPT-5.3 Instant 终于学会好好说话了,顺手对比了下同天发布的 Gemini 3.1 Flash-Lite
java·javascript
序安InToo39 分钟前
第6课|注释与代码风格
后端·操作系统·嵌入式
xyy12339 分钟前
C#: Newtonsoft.Json 到 System.Text.Json 迁移避坑指南
后端
洋洋技术笔记42 分钟前
Spring Boot Web MVC配置详解
spring boot·后端
JxWang0542 分钟前
VS Code 配置 Markdown 环境
后端
navms1 小时前
搞懂线程池,先把 Worker 机制啃明白
后端
JxWang051 小时前
离线数仓的优化及重构
后端
Nyarlathotep01131 小时前
gin01:初探gin的启动
后端·go