# 好友功能设计决策汇总 > **文档状态**:已确认 ✅ > **确认日期**: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` ```go FriendLimit: FriendLimitConfig{ Enabled: false, // 暂不启用 DefaultLimit: 0, // 不限制 MaxLimit: 10000, // 预留最大值 } ``` **理由**: - 初期不限制,简化实现 - 预留扩展性,后续可通过规则表根据用户等级、会员状态等动态限制 --- ### 1.2 重复请求冷却期 ⭐ **关键** ✅ **决策**:7天后可以再次发送 **配置位置**:`backend/services/friendService/config/friend_config.go` ```go TimeConstraints: TimeConstraints{ RejectionCooldownDays: 7, RejectionCooldownMillis: 7 * 24 * 60 * 60 * 1000, // 7天 } ``` **实现逻辑**: 1. 查询最近的请求记录(所有状态) 2. 如果状态为 `rejected`,检查 `processed_at` 距今是否超过 7 天 3. 未超过冷却期:返回 429 错误 + 剩余天数提示 4. 已超过冷却期:允许发送新请求 **使用方法**: ```go // 检查是否在冷却期内 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` ```go TimeConstraints: TimeConstraints{ RequestExpiryDays: 30, RequestExpiryMillis: 30 * 24 * 60 * 60 * 1000, // 30天 } ``` **实现逻辑**: - 创建请求时,设置 `expires_at = created_at + 30天` - 定时任务扫描过期请求,更新状态为 `expired` **使用方法**: ```go // 计算过期时间 expiryTime := config.GlobalFriendConfig.CalculateExpiryTime(createdAtMillis) // 检查是否已过期 if config.GlobalFriendConfig.IsExpired(createdAtMillis) { return errors.New("该请求已过期") } ``` --- ### 1.4 删除好友通知 ✅ **决策**:不通知对方(静默删除) **理由**: - 避免尴尬和不必要的冲突 - 符合大多数社交产品的设计习惯 - 对方在下次查看好友列表时会自然发现 **实现要点**: - 删除好友时,只删除双向关系记录 - 不发送任何推送通知 - 不在对方界面显示"已解除好友关系" --- ### 1.5 好友关系保留 ✅ **决策**:完全删除(物理删除) **理由**: - 简化数据模型,无需软删除字段 - 减少数据库存储压力 - 符合用户预期(删除就是删除) **实现要点**: ```sql -- 删除双向关系 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 参数**: ```json { "page": 1, "page_size": 20, "keyword": "小明" // 可选,搜索昵称或备注 } ``` #### 2.1.2 数据库索引设计 **friendships 表索引**(4个): ```sql -- 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个): ```sql -- 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 表索引**(用于关键词搜索): ```sql -- 用于昵称搜索 CREATE INDEX idx_fan_profiles_nickname ON fan_profiles(nickname); ``` #### 2.1.3 查询示例 **基础查询(无关键词)**: ```sql 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 ?; ``` **带关键词搜索**: ```sql 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 唯一索引防止重复 ```sql CONSTRAINT uk_friendships_user_friend_star UNIQUE (user_id, friend_id, star_id) ``` #### 2.2.2 事务使用场景 **场景1:接受好友请求** ```go 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:删除好友** ```go 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 并发场景处理 **场景1:A 和 B 同时向对方发送请求** - 允许两个请求都创建成功 - 任一方接受后,双方都成为好友 **场景2:A 删除好友的同时,B 也在删除** - 使用事务保证原子性 - 第一个事务成功,第二个事务会因为记录不存在而失败 - 返回友好的错误提示 --- ### 2.3 跨服务调用 ✅ **决策**:通过 Dubbo RPC 调用 #### 2.3.1 UserService 客户端接口 ```go 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 错误处理 ```go // 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 配置结构 ```go 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 使用方法 ```go // 初始化配置 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 **状态**:已确认,可以开始实现 ✅