You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Sequelize同User模型好友关系单向生效问题解决方案

Sequelize 自关联好友关系实现方案

问题背景

技术栈为Express + Sequelize + PostgreSQL,部署于Heroku,开发聊天应用时通过Friendship中间表实现User模型与自身的好友关联,存在问题:创建好友申请后,仅发起添加的用户侧可查询到好友记录,被添加用户的好友列表为空。调整关联配置后可部分满足需求,但实现不规范。

问题根因

原关联配置为单向关联:仅定义了*用户作为好友申请发起方(对应friendships表user字段)时的关联关系,没有覆盖用户作为好友申请接收方(对应friendships表friend字段)*时的关联关系。Sequelize查询好友列表时,只会匹配friendships.user = 当前用户ID的记录,被添加用户作为接收方时,其ID存储在friend字段中,自然无法查询到对应记录。后续调整的双关联方向逻辑正确,但别名定义模糊、未区分业务场景,导致实现不完善。

规范实现步骤

1. 修正模型关联配置

替换原有模糊的关联定义,按业务场景拆分双向关联,同时补全中间表的关联映射,避免别名冲突:

const { Op } = require('sequelize') // 后续查询需要用到操作符,提前导入

// 关联1:我作为发起方,添加的所有好友/发送的申请
User.belongsToMany(User, {
  as: 'sentFriendRequests',
  through: Friendship,
  foreignKey: 'user', // 关联中间表存发起方ID的user字段
  otherKey: 'friend' // 关联中间表存接收方ID的friend字段
})

// 关联2:我作为接收方,收到的所有好友申请/好友
User.belongsToMany(User, {
  as: 'receivedFriendRequests',
  through: Friendship,
  foreignKey: 'friend', // 关联中间表存接收方ID的friend字段
  otherKey: 'user' // 关联中间表存发起方ID的user字段
})

// 补全中间表与用户表的关联,方便后续查询申请双方信息
Friendship.belongsTo(User, { as: 'initiator', foreignKey: 'user' })
Friendship.belongsTo(User, { as: 'receiver', foreignKey: 'friend' })

2. 数据库层兜底优化

在friendships表的迁移文件中增加约束和索引,从数据库层面避免异常数据、提升查询速度:

// 创建完friendships表后追加以下代码
// 加联合唯一约束,避免同一对用户重复生成好友申请
await queryInterface.addConstraint('friendships', {
  fields: ['user', 'friend'],
  type: 'unique',
  name: 'unique_user_friend_relation'
})
// 加联合索引,提升好友列表、申请列表的查询效率
await queryInterface.addIndex('friendships', ['user', 'friend', 'status'])

3. 修正添加好友接口逻辑

原有逻辑缺少自添加校验、重复关系校验,且存在敏感信息泄露风险,补全后代码如下:

export const addFriend = async (req: Request, res: Response) => {
  try {
    const { friendId } = req.body;
    const userId = req.decodedToken?.id;

    // 禁止添加自己为好友
    if (userId === friendId) {
      return res.status(400).json({ error: '不能添加自己为好友' })
    }

    // 校验目标用户是否存在
    const targetUser = await User.findByPk(friendId, {
      attributes: { exclude: ['passwordHash'] },
    });
    if (!targetUser) {
      return res.status(404).json({ error: '目标用户不存在' })
    }

    // 校验是否已存在待确认/已通过的好友关系,避免重复申请
    const existRelation = await Friendship.findOne({
      where: {
        status: ['PENDING', 'ACCEPTED'],
        [Op.or]: [
          { user: userId, friend: friendId },
          { user: friendId, friend: userId }
        ]
      }
    })
    if (existRelation) {
      return res.status(400).json({ error: '已存在好友申请或好友关系' })
    }

    // 创建待确认好友申请
    await Friendship.create({
      user: userId,
      friend: friendId,
      status: 'PENDING',
    });

    return res.status(200).json({ status: 'success' });
  } catch (e) {
    res.status(500).json({ error: '服务端创建好友申请失败' });
    console.log(e);
  }
};

4. 好友列表查询实现

按业务场景分别查询即可,无需在好友通过时反向插入冗余数据:

  • 查我发送的待确认申请:关联sentFriendRequests,过滤中间表status='PENDING'
  • 查我收到的待确认申请:关联receivedFriendRequests,过滤中间表status='PENDING'
  • 查已通过的双向好友列表:合并两个方向status='ACCEPTED'的查询结果,去重后返回

已通过好友列表示例代码:

const getAcceptedFriendList = async (userId: string) => {
  // 查询我发起的已通过好友关系
  const sentRes = await User.findByPk(userId, {
    attributes: { exclude: ['passwordHash'] },
    include: [{
      model: User,
      as: 'sentFriendRequests',
      attributes: { exclude: ['passwordHash'] }, // 敏感字段必须排除
      through: { where: { status: 'ACCEPTED' } }
    }]
  })
  // 查询我收到的已通过好友关系
  const receivedRes = await User.findByPk(userId, {
    attributes: { exclude: ['passwordHash'] },
    include: [{
      model: User,
      as: 'receivedFriendRequests',
      attributes: { exclude: ['passwordHash'] }, // 敏感字段必须排除
      through: { where: { status: 'ACCEPTED' } }
    }]
  })
  // 合并两个列表即为完整的双向好友列表
  return [
    ...sentRes.sentFriendRequests,
    ...receivedRes.receivedFriendRequests
  ]
}

注意:所有返回用户信息的接口必须排除passwordHash字段,示例响应中出现密码哈希属于严重安全问题,必须修复。


内容的提问来源于stack exchange,提问作者AK96

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 14:39:53