topfans/backend/docs/好友功能设计决策汇总.md
2026-04-07 22:29:48 +08:00

13 KiB
Raw Permalink Blame History

好友功能设计决策汇总

文档状态:已确认
确认日期2026-01-06
目的:快速查阅所有设计决策和关键配置


📋 设计决策快速索引

问题 决策 配置位置 状态
好友数量限制 暂不限制,后续规则表动态配置 friend_config.go
重复请求冷却期 7天 friend_config.go
请求过期时间 30天 friend_config.go
删除好友通知 不通知对方 -
好友关系保留 完全删除(物理删除) -
好友列表优化 分页查询 + 数据库索引 见索引设计
并发控制 数据库事务 + 唯一索引 -
跨服务调用 Dubbo RPC -

1. 业务逻辑决策

1.1 好友数量限制

决策:暂不限制,后续引入规则表动态配置

配置位置backend/services/friendService/config/friend_config.go

FriendLimit: FriendLimitConfig{
    Enabled:      false, // 暂不启用
    DefaultLimit: 0,     // 不限制
    MaxLimit:     10000, // 预留最大值
}

理由

  • 初期不限制,简化实现
  • 预留扩展性,后续可通过规则表根据用户等级、会员状态等动态限制

1.2 重复请求冷却期 关键

决策7天后可以再次发送

配置位置backend/services/friendService/config/friend_config.go

TimeConstraints: TimeConstraints{
    RejectionCooldownDays:   7,
    RejectionCooldownMillis: 7 * 24 * 60 * 60 * 1000, // 7天
}

实现逻辑

  1. 查询最近的请求记录(所有状态)
  2. 如果状态为 rejected,检查 processed_at 距今是否超过 7 天
  3. 未超过冷却期:返回 429 错误 + 剩余天数提示
  4. 已超过冷却期:允许发送新请求

使用方法

// 检查是否在冷却期内
if config.GlobalFriendConfig.IsInCooldownPeriod(rejectedAtMillis) {
    remainingDays := config.GlobalFriendConfig.CalculateRemainingCooldownDays(rejectedAtMillis)
    return fmt.Errorf("请求已被拒绝,请 %d 天后再试", remainingDays)
}

1.3 请求过期时间

决策30天

配置位置backend/services/friendService/config/friend_config.go

TimeConstraints: TimeConstraints{
    RequestExpiryDays:   30,
    RequestExpiryMillis: 30 * 24 * 60 * 60 * 1000, // 30天
}

实现逻辑

  • 创建请求时,设置 expires_at = created_at + 30天
  • 定时任务扫描过期请求,更新状态为 expired

使用方法

// 计算过期时间
expiryTime := config.GlobalFriendConfig.CalculateExpiryTime(createdAtMillis)

// 检查是否已过期
if config.GlobalFriendConfig.IsExpired(createdAtMillis) {
    return errors.New("该请求已过期")
}

1.4 删除好友通知

决策:不通知对方(静默删除)

理由

  • 避免尴尬和不必要的冲突
  • 符合大多数社交产品的设计习惯
  • 对方在下次查看好友列表时会自然发现

实现要点

  • 删除好友时,只删除双向关系记录
  • 不发送任何推送通知
  • 不在对方界面显示"已解除好友关系"

1.5 好友关系保留

决策:完全删除(物理删除)

理由

  • 简化数据模型,无需软删除字段
  • 减少数据库存储压力
  • 符合用户预期(删除就是删除)

实现要点

-- 删除双向关系
DELETE FROM friendships 
WHERE (user_id = ? AND friend_id = ? AND star_id = ?)
   OR (user_id = ? AND friend_id = ? AND star_id = ?);

-- 同时更新 fan_profiles 表的 social 字段
UPDATE fan_profiles 
SET social = social - 1 
WHERE (user_id = ? OR user_id = ?) AND star_id = ?;

2. 技术实现决策

2.1 好友列表查询优化

决策:支持分页查询 + 添加数据库索引

2.1.1 分页查询

API 参数

{
  "page": 1,
  "page_size": 20,
  "keyword": "小明"  // 可选,搜索昵称或备注
}

2.1.2 数据库索引设计

friendships 表索引4个

-- 1. 复合唯一索引(防止重复添加)
CREATE UNIQUE INDEX uk_friendships_user_friend_star 
ON friendships(user_id, friend_id, star_id);

-- 2. 用于基础好友列表查询
CREATE INDEX idx_friendships_user_star_status 
ON friendships(user_id, star_id, status);

-- 3. 用于按时间排序的好友列表查询
CREATE INDEX idx_friendships_user_star_created 
ON friendships(user_id, star_id, created_at DESC);

-- 4. 覆盖索引,包含常用查询字段,避免回表查询
CREATE INDEX idx_friendships_list_query 
ON friendships(user_id, star_id, status, friend_id, created_at, remark);

friend_requests 表索引5个

-- 1. 用于查询收到的请求(按状态筛选)
CREATE INDEX idx_friend_requests_to_status 
ON friend_requests(to_user_id, status, created_at DESC);

-- 2. 用于查询发出的请求
CREATE INDEX idx_friend_requests_from_status 
ON friend_requests(from_user_id, status, created_at DESC);

-- 3. 用于查询特定明星下的请求
CREATE INDEX idx_friend_requests_star 
ON friend_requests(star_id);

-- 4. 用于定时任务扫描过期请求
CREATE INDEX idx_friend_requests_expires 
ON friend_requests(expires_at, status);

-- 5. 用于查询两个用户之间的最近请求(防骚扰机制)
CREATE INDEX idx_friend_requests_users_star 
ON friend_requests(from_user_id, to_user_id, star_id, created_at DESC);

fan_profiles 表索引(用于关键词搜索):

-- 用于昵称搜索
CREATE INDEX idx_fan_profiles_nickname 
ON fan_profiles(nickname);

2.1.3 查询示例

基础查询(无关键词)

SELECT f.friend_id, f.remark, f.intimacy, f.created_at,
       fp.nickname, fp.avatar_url, fp.fan_level
FROM friendships f
LEFT JOIN fan_profiles fp ON f.friend_id = fp.user_id AND f.star_id = fp.star_id
WHERE f.user_id = ? AND f.star_id = ? AND f.status = 'accepted'
ORDER BY f.created_at DESC
LIMIT ? OFFSET ?;

带关键词搜索

SELECT f.friend_id, f.remark, f.intimacy, f.created_at,
       fp.nickname, fp.avatar_url, fp.fan_level
FROM friendships f
LEFT JOIN fan_profiles fp ON f.friend_id = fp.user_id AND f.star_id = fp.star_id
WHERE f.user_id = ? AND f.star_id = ? AND f.status = 'accepted'
  AND (f.remark LIKE ? OR fp.nickname LIKE ?)
ORDER BY f.created_at DESC
LIMIT ? OFFSET ?;

2.2 并发控制

决策:使用数据库事务保证原子性

2.2.1 唯一索引防止重复

CONSTRAINT uk_friendships_user_friend_star 
    UNIQUE (user_id, friend_id, star_id)

2.2.2 事务使用场景

场景1接受好友请求

tx := db.Begin()
defer func() {
    if r := recover(); r != nil {
        tx.Rollback()
    }
}()

// 1. 更新请求状态
tx.Model(&FriendRequest{}).Where("id = ?", requestID).
    Updates(map[string]interface{}{
        "status": "accepted",
        "processed_at": time.Now().UnixMilli(),
    })

// 2. 创建双向好友关系
tx.Create(&Friendship{UserID: userA, FriendID: userB, StarID: starID})
tx.Create(&Friendship{UserID: userB, FriendID: userA, StarID: starID})

// 3. 更新 social 字段
tx.Model(&FanProfile{}).Where("user_id = ? AND star_id = ?", userA, starID).
    Update("social", gorm.Expr("social + ?", 1))
tx.Model(&FanProfile{}).Where("user_id = ? AND star_id = ?", userB, starID).
    Update("social", gorm.Expr("social + ?", 1))

tx.Commit()

场景2删除好友

tx := db.Begin()
defer func() {
    if r := recover(); r != nil {
        tx.Rollback()
    }
}()

// 1. 删除双向好友关系
tx.Where("(user_id = ? AND friend_id = ? AND star_id = ?) OR "+
         "(user_id = ? AND friend_id = ? AND star_id = ?)",
    userA, userB, starID, userB, userA, starID).
    Delete(&Friendship{})

// 2. 更新 social 字段
tx.Model(&FanProfile{}).Where("user_id = ? AND star_id = ?", userA, starID).
    Update("social", gorm.Expr("social - ?", 1))
tx.Model(&FanProfile{}).Where("user_id = ? AND star_id = ?", userB, starID).
    Update("social", gorm.Expr("social - ?", 1))

tx.Commit()

2.2.3 并发场景处理

场景1A 和 B 同时向对方发送请求

  • 允许两个请求都创建成功
  • 任一方接受后,双方都成为好友

场景2A 删除好友的同时B 也在删除

  • 使用事务保证原子性
  • 第一个事务成功,第二个事务会因为记录不存在而失败
  • 返回友好的错误提示

2.3 跨服务调用

决策:通过 Dubbo RPC 调用

2.3.1 UserService 客户端接口

type UserServiceClient interface {
    // 验证用户是否存在
    ValidateUser(ctx context.Context, userID int64) (bool, error)
    
    // 验证用户是否有指定的粉丝身份
    ValidateFanProfile(ctx context.Context, userID, starID int64) (bool, error)
    
    // 批量获取用户信息(用于好友列表展示)
    GetUsersByIDs(ctx context.Context, userIDs []int64) ([]*pb.UserInfo, error)
}

2.3.2 调用时机

  1. 发送好友请求前

    • 验证目标用户是否存在
    • 验证目标用户是否有该粉丝身份
  2. 获取好友列表时

    • 批量获取好友的用户信息(避免 N+1 问题)

2.3.3 错误处理

// RPC 调用失败
if err != nil {
    return nil, errors.New("服务暂时不可用,请稍后重试")
}

// 用户不存在
if !exists {
    return nil, errors.New("用户不存在")
}

// 用户没有该粉丝身份
if !hasFanProfile {
    return nil, errors.New("对方没有该粉丝身份")
}

3. 配置文件说明

3.1 配置文件位置

backend/services/friendService/config/friend_config.go

3.2 配置结构

type FriendConfig struct {
    TimeConstraints TimeConstraints  // 时间限制配置
    FriendLimit     FriendLimitConfig // 好友数量限制(预留)
}

type TimeConstraints struct {
    RejectionCooldownDays   int   // 被拒绝后的冷却期(天数) = 7
    RequestExpiryDays       int   // 好友请求的过期时间(天数) = 30
    RejectionCooldownMillis int64 // 被拒绝后的冷却期(毫秒)
    RequestExpiryMillis     int64 // 好友请求的过期时间(毫秒)
}

type FriendLimitConfig struct {
    Enabled      bool // 是否启用好友数量限制 = false
    DefaultLimit int  // 默认好友数量上限 = 0 (不限制)
    MaxLimit     int  // 最大好友数量上限 = 10000
}

3.3 使用方法

// 初始化配置
config.InitFriendConfig()

// 获取全局配置
cfg := config.GlobalFriendConfig

// 检查是否在冷却期内
if cfg.IsInCooldownPeriod(rejectedAtMillis) {
    remainingDays := cfg.CalculateRemainingCooldownDays(rejectedAtMillis)
    return fmt.Errorf("请求已被拒绝,请 %d 天后再试", remainingDays)
}

// 计算过期时间
expiryTime := cfg.CalculateExpiryTime(createdAtMillis)

// 检查是否已过期
if cfg.IsExpired(createdAtMillis) {
    return errors.New("该请求已过期")
}

4. 错误码定义

错误码 错误信息 HTTP 状态码 说明
20001 用户不存在 404 目标用户不存在
20002 对方没有该粉丝身份 400 目标用户没有指定的粉丝身份
20003 已经是好友 400 不能重复添加好友
20004 已发送过好友请求 400 已有未处理的好友请求
20005 好友请求不存在 404 请求 ID 不存在
20006 该请求已被处理 400 请求已经被接受或拒绝
20007 该请求已过期 400 请求已过期
20008 好友关系不存在 404 不是好友关系
20009 不能添加自己为好友 400 参数错误
20010 已达到好友数量上限 400 好友数量已达上限
20011 请求被拒绝,冷却期内 429 请求已被拒绝请X天后再试

5. 实现检查清单

5.1 数据层

  • 创建 Friendship 模型
  • 创建 FriendRequest 模型
  • 实现 FriendRepository 接口
  • 创建数据库迁移脚本
  • 添加所有索引friendships 4个friend_requests 5个

5.2 服务层

  • 定义 Proto 文件
  • 实现 SendFriendRequest 方法(含防骚扰机制)
  • 实现 GetFriendRequests 方法(分页)
  • 实现 HandleFriendRequest 方法(事务)
  • 实现 GetFriendList 方法(分页 + 关键词搜索)
  • 实现 DeleteFriend 方法(事务)
  • 实现 SetFriendRemark 方法
  • 创建 userService RPC 客户端

5.3 网关层

  • 创建 DTO 定义
  • 创建转换器
  • 实现 Controller
  • 配置路由

5.4 测试

  • 单元测试
  • 集成测试
  • 性能测试

6. 后续优化方向

6.1 短期优化

  • 添加 Redis 缓存(缓存好友列表)
  • 实现定时任务(扫描过期请求)
  • 添加监控和日志

6.2 长期优化

  • 引入规则表动态配置好友数量限制
  • 实现好友推荐功能
  • 实现屏蔽功能
  • 实现亲密度系统

文档版本v1.0
最后更新2026-01-06
状态:已确认,可以开始实现