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

发布于:2025-06-16 ⋅ 阅读:(24) ⋅ 点赞:(0)

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)

@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: 视频ID
  • vlogCover: 视频封面
  • 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;
}

服务层关键实现步骤

  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提供的方法命名约定来定义查询方法:

// 通过实现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自定义查询方法详解

方法签名分析

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: 用于匹配消息接收者的ID
  • Pageable 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 ?


网站公告

今日签到

点亮在社区的每一天
去签到