13 KiB
13 KiB
好友功能设计决策汇总
文档状态:已确认 ✅
确认日期: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天
}
实现逻辑:
- 查询最近的请求记录(所有状态)
- 如果状态为
rejected,检查processed_at距今是否超过 7 天 - 未超过冷却期:返回 429 错误 + 剩余天数提示
- 已超过冷却期:允许发送新请求
使用方法:
// 检查是否在冷却期内
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 并发场景处理
场景1:A 和 B 同时向对方发送请求
- 允许两个请求都创建成功
- 任一方接受后,双方都成为好友
场景2:A 删除好友的同时,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 调用时机
-
发送好友请求前:
- 验证目标用户是否存在
- 验证目标用户是否有该粉丝身份
-
获取好友列表时:
- 批量获取好友的用户信息(避免 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
状态:已确认,可以开始实现 ✅