47 KiB
好友功能设计方案(已确认版)
文档状态:设计已确认 ✅
创建时间:2026-01-05
确认时间:2026-01-06
目标:定义好友系统的数据库设计、API 接口和业务流程
📋 目录
1. 功能概述
1.1 核心功能
- ✅ 发送好友请求:用户 A 向用户 B 发送好友请求
- ✅ 接受好友请求:用户 B 接受用户 A 的好友请求
- ✅ 拒绝好友请求:用户 B 拒绝用户 A 的好友请求
- ✅ 删除好友:用户可以删除已添加的好友
- ✅ 查看好友列表:查看自己的好友列表
- ✅ 查看好友请求列表:查看收到的好友请求
1.2 设计原则
-
粉丝身份隔离:好友关系基于粉丝身份(star_id)
- 用户在不同明星的身份下可以有不同的好友圈
- 例如:用户 A 作为"肖战粉丝"的好友 ≠ 作为"王一博粉丝"的好友
-
双向关系:好友关系是双向的
- A 添加 B 为好友,B 也会自动成为 A 的好友
- 删除好友时,双方都会失去好友关系
-
请求-确认机制:
- A 发送请求 → B 收到请求 → B 接受/拒绝 → 建立/不建立好友关系
-
数据一致性:
- 使用外键约束保证数据完整性
- 使用事务保证操作原子性
2. 数据库设计
2.1 好友关系表(friendships)
用途:存储已确认的好友关系
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY | 主键,自增 |
| user_id | BIGINT | NOT NULL, FK | 用户 ID |
| friend_id | BIGINT | NOT NULL, FK | 好友用户 ID |
| star_id | BIGINT | NOT NULL, FK | 粉丝身份(明星 ID) |
| status | VARCHAR(20) | NOT NULL | 状态:accepted(已接受)、blocked(已屏蔽) |
| remark | VARCHAR(50) | NULLABLE | 备注名(好友昵称) |
| intimacy | INT | DEFAULT 0 | 亲密度(预留字段) |
| created_at | BIGINT | NOT NULL | 创建时间(毫秒时间戳) |
| updated_at | BIGINT | NOT NULL | 更新时间(毫秒时间戳) |
索引设计:
-- 复合唯一索引(防止重复添加)
UNIQUE INDEX uk_friendships_user_friend_star (user_id, friend_id, star_id)
-- 查询索引(优化好友列表查询性能)
-- 索引1: 用于基础好友列表查询
INDEX idx_friendships_user_star_status (user_id, star_id, status)
-- 索引2: 用于按时间排序的好友列表查询
INDEX idx_friendships_user_star_created (user_id, star_id, created_at DESC)
-- 索引3: 覆盖索引,包含常用查询字段,避免回表查询
INDEX idx_friendships_list_query (user_id, star_id, status, friend_id, created_at, remark)
-- 索引4: 反向查询索引(查询谁把我加为好友)
INDEX idx_friendships_friend_star (friend_id, star_id)
外键约束:
-- 用户外键
CONSTRAINT fk_friendships_user
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE CASCADE
-- 好友外键
CONSTRAINT fk_friendships_friend
FOREIGN KEY (friend_id) REFERENCES users(id)
ON DELETE CASCADE
-- 明星外键
CONSTRAINT fk_friendships_star
FOREIGN KEY (star_id) REFERENCES stars(star_id)
ON DELETE CASCADE
检查约束:
-- 不能添加自己为好友
CONSTRAINT chk_friendships_not_self
CHECK (user_id != friend_id)
设计说明:
- ✅ 双向存储:A→B 和 B→A 各存一条记录,便于查询
- ✅ 级联删除:用户被删除时,相关好友关系自动清理
- ✅ status 字段预留 blocked 状态,支持未来的屏蔽功能
2.2 好友请求表(friend_requests)
用途:存储好友请求的历史记录
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY | 主键,自增 |
| from_user_id | BIGINT | NOT NULL, FK | 请求发送者 ID |
| to_user_id | BIGINT | NOT NULL, FK | 请求接收者 ID |
| star_id | BIGINT | NOT NULL, FK | 粉丝身份(明星 ID) |
| message | VARCHAR(200) | NULLABLE | 请求附带消息 |
| status | VARCHAR(20) | NOT NULL | 状态:pending、accepted、rejected、expired |
| created_at | BIGINT | NOT NULL | 创建时间(毫秒时间戳) |
| updated_at | BIGINT | NOT NULL | 更新时间(毫秒时间戳) |
| expires_at | BIGINT | NULLABLE | 过期时间(毫秒时间戳) |
| processed_at | BIGINT | NULLABLE | 处理时间(毫秒时间戳) |
索引设计:
-- 查询索引(优化好友请求列表查询性能)
-- 索引1: 用于查询收到的请求(按状态筛选)
INDEX idx_friend_requests_to_status (to_user_id, status, created_at DESC)
-- 索引2: 用于查询发出的请求
INDEX idx_friend_requests_from_status (from_user_id, status, created_at DESC)
-- 索引3: 用于查询特定明星下的请求
INDEX idx_friend_requests_star (star_id)
-- 索引4: 用于定时任务扫描过期请求
INDEX idx_friend_requests_expires (expires_at, status)
-- 索引5: 用于查询两个用户之间的最近请求(防骚扰机制)
INDEX idx_friend_requests_users_star (from_user_id, to_user_id, star_id, created_at DESC)
外键约束:
-- 发送者外键
CONSTRAINT fk_friend_requests_from
FOREIGN KEY (from_user_id) REFERENCES users(id)
ON DELETE CASCADE
-- 接收者外键
CONSTRAINT fk_friend_requests_to
FOREIGN KEY (to_user_id) REFERENCES users(id)
ON DELETE CASCADE
-- 明星外键
CONSTRAINT fk_friend_requests_star
FOREIGN KEY (star_id) REFERENCES stars(star_id)
ON DELETE CASCADE
检查约束:
-- 不能请求自己为好友
CONSTRAINT chk_friend_requests_not_self
CHECK (from_user_id != to_user_id)
设计说明:
- ✅ 保留历史记录:即使请求被处理,记录仍保留(便于追溯)
- ✅ 过期机制:请求 30 天后自动过期(通过定时任务更新 status)
- ✅ processed_at:记录何时处理,便于统计响应时间
3. 业务流程设计
3.1 发送好友请求流程
┌─────────────────────────────────────────────────────────────┐
│ 发送好友请求 │
└─────────────────────────────────────────────────────────────┘
用户 A 系统 用户 B
│ │ │
│──① 点击"添加好友"──────────→│ │
│ │ │
│ │──② 验证参数────────────────│
│ │ - A 和 B 是否是同一人? │
│ │ - B 是否存在? │
│ │ - B 是否有该粉丝身份? │
│ │ │
│ │──③ 检查关系状态────────────│
│ │ - 是否已经是好友? │
│ │ - 是否有未处理的请求? │
│ │ │
│ │──④ 创建好友请求────────────│
│ │ INSERT INTO friend_requests│
│ │ status = 'pending' │
│ │ │
│←─⑤ 返回成功──────────────│ │
│ "好友请求已发送" │ │
│ │ │
│ │──⑥ 推送通知(可选)────────→│
│ │ "您收到来自 A 的好友请求" │
│ │ │
关键验证:
- ✅ 不能添加自己为好友
- ✅ 目标用户必须存在
- ✅ 目标用户必须有该粉丝身份
- ✅ 检查是否已经是好友
- ✅ 检查历史请求记录(防骚扰机制):
- 查询最近的请求记录(所有状态)
- 如果有
pending请求 → 提示"已发送过请求" - 如果有
rejected请求 → 检查是否过了冷却期(7天) - 如果有
accepted请求 → 提示"已经是好友"
异常处理:
- 目标用户不存在 → 返回 404 "用户不存在"
- 目标用户没有该粉丝身份 → 返回 400 "对方没有该粉丝身份"
- 已经是好友 → 返回 400 "已经是好友"
- 已有未处理的请求 → 返回 400 "已发送过好友请求,请等待对方处理"
- 请求被拒绝且在冷却期内 → 返回 400 "请求已被拒绝,请 X 天后再试"
3.1.1 防骚扰机制详解
问题:如何防止用户被重复骚扰?
解决方案:基于请求历史的冷却期机制
检查逻辑流程图
发送好友请求
↓
查询最近的请求记录(同 star_id 下)
↓
是否有历史请求?
├─ 否 → ✅ 允许发送
└─ 是 ↓
检查请求状态
├─ pending → ❌ "已发送过请求,请等待对方处理"
├─ accepted → ❌ "已经是好友"
├─ expired → ✅ 允许发送新请求
└─ rejected ↓
计算距离 processed_at 的时间
├─ < 7天 → ❌ "请求已被拒绝,请 X 天后再试"
└─ ≥ 7天 → ✅ 允许发送新请求
实现代码示例
func (s *friendService) SendFriendRequest(req *pb.SendFriendRequestRequest, userID, starID int64) (*pb.SendFriendRequestResponse, error) {
// ... 前置验证 ...
// 检查历史请求记录
latestRequest, err := s.friendRepo.GetLatestRequest(userID, req.FriendUserId, starID)
if err != nil && !errors.Is(err, appErrors.ErrRequestNotFound) {
return nil, err
}
if latestRequest != nil {
switch latestRequest.Status {
case "pending":
return nil, errors.New("已发送过好友请求,请等待对方处理")
case "accepted":
return nil, errors.New("已经是好友")
case "rejected":
// 检查冷却期(7天 = 7 * 24 * 60 * 60 * 1000 毫秒)
cooldownPeriod := int64(7 * 24 * 60 * 60 * 1000)
now := time.Now().UnixMilli()
if latestRequest.ProcessedAt != nil {
timeSinceRejection := now - *latestRequest.ProcessedAt
if timeSinceRejection < cooldownPeriod {
remainingDays := (cooldownPeriod - timeSinceRejection) / (24 * 60 * 60 * 1000)
return nil, fmt.Errorf("请求已被拒绝,请 %d 天后再试", remainingDays+1)
}
}
case "expired":
// 过期的请求可以重新发送
break
}
}
// 创建新请求...
}
配置参数
所有时间限制配置已统一管理在 backend/services/friendService/config/friend_config.go 中:
// FriendConfig 好友功能配置
type FriendConfig struct {
TimeConstraints TimeConstraints // 时间限制配置
FriendLimit FriendLimitConfig // 好友数量限制(预留)
}
// TimeConstraints 时间约束配置
type TimeConstraints struct {
RejectionCooldownDays int // 被拒绝后的冷却期(天数) = 7天
RequestExpiryDays int // 好友请求的过期时间(天数) = 30天
RejectionCooldownMillis int64 // 被拒绝后的冷却期(毫秒)
RequestExpiryMillis int64 // 好友请求的过期时间(毫秒)
}
配置说明:
- ✅ 统一管理所有时间限制,便于维护
- ✅ 便于后续迁移到规则表进行动态配置
- ✅ 提供了丰富的辅助方法(计算剩余天数、检查是否过期等)
使用示例:
// 初始化配置
config.InitFriendConfig()
// 检查是否在冷却期内
if config.GlobalFriendConfig.IsInCooldownPeriod(rejectedAtMillis) {
remainingDays := config.GlobalFriendConfig.CalculateRemainingCooldownDays(rejectedAtMillis)
return fmt.Errorf("请求已被拒绝,请 %d 天后再试", remainingDays)
}
3.2 接受好友请求流程
┌─────────────────────────────────────────────────────────────┐
│ 接受好友请求 │
└─────────────────────────────────────────────────────────────┘
用户 A 系统 用户 B
│ │ │
│ │←─① 点击"接受"──────────────│
│ │ │
│ │──② 验证请求────────────────│
│ │ - 请求是否存在? │
│ │ - 请求状态是否为 pending? │
│ │ - 请求是否已过期? │
│ │ │
│ │──③ 开启事务────────────────│
│ │ │
│ │──④ 更新请求状态────────────│
│ │ UPDATE friend_requests │
│ │ status = 'accepted' │
│ │ processed_at = now() │
│ │ │
│ │──⑤ 创建双向好友关系─────────│
│ │ INSERT INTO friendships │
│ │ (A→B) status='accepted' │
│ │ (B→A) status='accepted' │
│ │ │
│ │──⑥ 更新好友数量────────────│
│ │ UPDATE fan_profiles │
│ │ social = social + 1 │
│ │ (A 和 B 的 social 字段) │
│ │ │
│ │──⑦ 提交事务────────────────│
│ │ │
│←─⑧ 推送通知(可选)───────────│ │
│ "B 接受了您的好友请求" │ │
│ │ │
│ │──⑨ 返回成功─────────────→│
│ │ "已添加为好友" │
│ │ │
关键操作:
- ✅ 使用事务保证原子性
- ✅ 创建双向好友关系(A→B 和 B→A)
- ✅ 更新 fan_profiles 表的 social 字段(好友数量)
- ✅ 更新请求状态为 accepted
异常处理:
- 请求不存在 → 返回 404 "好友请求不存在"
- 请求已被处理 → 返回 400 "该请求已被处理"
- 请求已过期 → 返回 400 "该请求已过期"
- 事务失败 → 回滚所有操作,返回 500 "操作失败"
3.3 拒绝好友请求流程
┌─────────────────────────────────────────────────────────────┐
│ 拒绝好友请求 │
└─────────────────────────────────────────────────────────────┘
用户 A 系统 用户 B
│ │ │
│ │←─① 点击"拒绝"──────────────│
│ │ │
│ │──② 验证请求────────────────│
│ │ - 请求是否存在? │
│ │ - 请求状态是否为 pending? │
│ │ │
│ │──③ 更新请求状态────────────│
│ │ UPDATE friend_requests │
│ │ status = 'rejected' │
│ │ processed_at = now() │
│ │ │
│ │──④ 返回成功─────────────→│
│ │ "已拒绝好友请求" │
│ │ │
关键操作:
- ✅ 只更新请求状态,不创建好友关系
- ✅ 不通知请求发送者(避免尴尬)
- ✅ 保留拒绝记录(可用于防骚扰)
3.4 删除好友流程
┌─────────────────────────────────────────────────────────────┐
│ 删除好友 │
└─────────────────────────────────────────────────────────────┘
用户 A 系统 用户 B
│ │ │
│──① 点击"删除好友"──────────→│ │
│ │ │
│ │──② 验证好友关系────────────│
│ │ - 是否是好友? │
│ │ - 关系是否存在? │
│ │ │
│ │──③ 开启事务────────────────│
│ │ │
│ │──④ 删除双向好友关系─────────│
│ │ DELETE FROM friendships │
│ │ WHERE (user_id=A AND friend_id=B)│
│ │ OR (user_id=B AND friend_id=A)│
│ │ AND star_id = X │
│ │ │
│ │──⑤ 更新好友数量────────────│
│ │ UPDATE fan_profiles │
│ │ social = social - 1 │
│ │ (A 和 B 的 social 字段) │
│ │ │
│ │──⑥ 提交事务────────────────│
│ │ │
│←─⑦ 返回成功──────────────│ │
│ "已删除好友" │ │
│ │ │
│ │──⑧ 推送通知(可选)────────→│
│ │ "A 已将您从好友列表移除" │
│ │ │
关键操作:
- ✅ 使用事务保证原子性
- ✅ 删除双向好友关系(A→B 和 B→A)
- ✅ 更新 fan_profiles 表的 social 字段(减少好友数量)
- ✅ 可选通知对方(根据产品需求决定)
异常处理:
- 好友关系不存在 → 返回 404 "好友关系不存在"
- 事务失败 → 回滚所有操作,返回 500 "操作失败"
4. API 接口设计
4.1 发送好友请求
接口:POST /api/v1/friends/requests
请求头:
Authorization: Bearer <access_token>
Content-Type: application/json
请求体:
{
"friend_user_id": 123,
"message": "你好,我是你的粉丝,想和你成为好友" // 可选
}
成功响应(200):
{
"code": 200,
"message": "ok",
"data": {
"request_id": 456,
"status": "pending",
"created_at": 1704441600000
}
}
错误响应:
// 400 - 已经是好友
{
"code": 400,
"message": "已经是好友"
}
// 400 - 已发送过请求
{
"code": 400,
"message": "已发送过好友请求,请等待对方处理"
}
// 404 - 用户不存在
{
"code": 404,
"message": "用户不存在"
}
4.2 获取好友请求列表
接口:GET /api/v1/friends/requests
请求头:
Authorization: Bearer <access_token>
查询参数:
type=received // 收到的请求(默认)或 sent(发出的请求)
status=pending // 请求状态(可选):pending, accepted, rejected, all
page=1 // 页码(可选,默认 1)
page_size=20 // 每页数量(可选,默认 20)
成功响应(200):
{
"code": 200,
"message": "ok",
"data": {
"items": [
{
"request_id": 456,
"user_id": 123,
"nickname": "粉丝小明",
"avatar_url": "https://...",
"message": "你好,我是你的粉丝",
"status": "pending",
"created_at": 1704441600000,
"expires_at": 1707033600000
}
],
"total": 10,
"page": 1,
"page_size": 20
}
}
4.3 处理好友请求(接受/拒绝)
接口:POST /api/v1/friends/requests/{request_id}/handle
请求头:
Authorization: Bearer <access_token>
Content-Type: application/json
请求体:
{
"action": "accept" // accept(接受)或 reject(拒绝)
}
成功响应(200):
{
"code": 200,
"message": "ok",
"data": {
"action": "accept",
"friendship_created": true // 仅在 accept 时有此字段
}
}
错误响应:
// 404 - 请求不存在
{
"code": 404,
"message": "好友请求不存在"
}
// 400 - 请求已被处理
{
"code": 400,
"message": "该请求已被处理"
}
4.4 获取好友列表
接口:GET /api/v1/friends
请求头:
Authorization: Bearer <access_token>
查询参数:
keyword=小明 // 搜索关键词(可选)
page=1 // 页码(可选,默认 1)
page_size=20 // 每页数量(可选,默认 20)
成功响应(200):
{
"code": 200,
"message": "ok",
"data": {
"items": [
{
"user_id": 123,
"nickname": "粉丝小明",
"avatar_url": "https://...",
"remark": "我的好友", // 备注名(如果有)
"fan_level": 2,
"intimacy": 100, // 亲密度(预留字段)
"created_at": 1704441600000 // 成为好友的时间
}
],
"total": 50,
"page": 1,
"page_size": 20
}
}
4.5 删除好友
接口:DELETE /api/v1/friends/{user_id}
请求头:
Authorization: Bearer <access_token>
成功响应(200):
{
"code": 200,
"message": "ok",
"data": {}
}
错误响应:
// 404 - 好友关系不存在
{
"code": 404,
"message": "好友关系不存在"
}
4.6 设置好友备注
接口:PUT /api/v1/friends/{user_id}/remark
请求头:
Authorization: Bearer <access_token>
Content-Type: application/json
请求体:
{
"remark": "我的好朋友" // 最大 50 个字符
}
成功响应(200):
{
"code": 200,
"message": "ok",
"data": {
"remark": "我的好朋友"
}
}
5. 状态机设计
5.1 好友请求状态转换
┌─────────────┐
│ │
创建请求 │ pending │
────────→│ (待处理) │
│ │
└─────┬───┬───┘
│ │
接受 │ │ 拒绝
↓ │ │ ↓
┌──────────┐ │ │ ┌──────────┐
│ │ │ │ │ │
│ accepted │ │ │ │ rejected │
│ (已接受) │ │ │ │ (已拒绝) │
│ │ │ │ │ │
└──────────┘ │ │ └──────────┘
│ │
│ │ 30天后
│ └────────→
│ ┌──────────┐
│ │ │
└────────→│ expired │
│ (已过期) │
│ │
└──────────┘
状态说明:
pending:待处理(初始状态)accepted:已接受(终态,好友关系已建立)rejected:已拒绝(终态)expired:已过期(终态,通过定时任务自动更新)
状态转换规则:
- ✅
pending→accepted:接受好友请求 - ✅
pending→rejected:拒绝好友请求 - ✅
pending→expired:30 天未处理自动过期 - ❌ 终态不能再转换
5.2 好友关系状态
┌──────────┐
创建关系│ │
───────→│ accepted │──┐ 删除好友
│ (已接受) │ │ ────────→ [关系记录被删除]
│ │←─┘
└─────┬────┘
│
│ 屏蔽(预留功能)
↓
┌──────────┐
│ │
│ blocked │
│ (已屏蔽) │
│ │
└──────────┘
状态说明:
accepted:正常好友关系blocked:已屏蔽(预留字段,暂不实现)
6. 实现步骤
阶段 1:数据层(Model & Repository)
步骤 1.1:创建数据模型
文件:backend/pkg/models/friend.go
内容:
- 定义
Friendship结构体 - 定义
FriendRequest结构体 - 添加
BeforeCreate和BeforeUpdate钩子 - 添加表名映射方法
步骤 1.2:创建 Repository
文件:backend/services/friendService/repository/friend_repository.go
接口定义:
type FriendRepository interface {
// 好友请求相关
CreateRequest(request *models.FriendRequest) error
GetRequestByID(requestID int64) (*models.FriendRequest, error)
GetRequestsByUser(userID, starID int64, requestType string, status string) ([]*models.FriendRequest, error)
GetLatestRequest(fromUserID, toUserID, starID int64) (*models.FriendRequest, error) // 新增:获取最近的请求记录
UpdateRequestStatus(requestID int64, status string) error
// 好友关系相关
CreateFriendship(friendship *models.Friendship) error
GetFriendship(userID, friendID, starID int64) (*models.Friendship, error)
GetFriendsByUser(userID, starID int64) ([]*models.Friendship, error)
DeleteFriendship(userID, friendID, starID int64) error
UpdateRemark(userID, friendID, starID int64, remark string) error
CountFriends(userID, starID int64) (int64, error)
}
任务清单:
- 实现
CreateRequest方法 - 实现
GetRequestByID方法 - 实现
GetRequestsByUser方法 - 实现
GetLatestRequest方法(获取两个用户之间最近的请求记录) - 实现
UpdateRequestStatus方法 - 实现
CreateFriendship方法 - 实现
GetFriendship方法 - 实现
GetFriendsByUser方法 - 实现
DeleteFriendship方法 - 实现
UpdateRemark方法 - 实现
CountFriends方法
阶段 2:服务层(Service)
步骤 2.1:定义 Proto
文件:backend/proto/friend/friend.proto
内容:
- 定义好友请求相关的 Message
- 定义好友关系相关的 Message
- 定义 FriendService RPC 接口
步骤 2.2:实现 Service
文件:backend/services/friendService/service/friend_service.go
接口定义:
type FriendService interface {
// 发送好友请求
SendFriendRequest(req *pb.SendFriendRequestRequest, userID, starID int64) (*pb.SendFriendRequestResponse, error)
// 获取好友请求列表
GetFriendRequests(req *pb.GetFriendRequestsRequest, userID, starID int64) (*pb.GetFriendRequestsResponse, error)
// 处理好友请求(接受/拒绝)
HandleFriendRequest(req *pb.HandleFriendRequestRequest, userID, starID int64) (*pb.HandleFriendRequestResponse, error)
// 获取好友列表
GetFriendList(req *pb.GetFriendListRequest, userID, starID int64) (*pb.GetFriendListResponse, error)
// 删除好友
DeleteFriend(req *pb.DeleteFriendRequest, userID, starID int64) (*pb.DeleteFriendResponse, error)
// 设置好友备注
SetFriendRemark(req *pb.SetFriendRemarkRequest, userID, starID int64) (*pb.SetFriendRemarkResponse, error)
}
任务清单:
-
实现
SendFriendRequest方法- 参数验证(不能添加自己为好友)
- 检查目标用户是否存在(RPC 调用 userService)
- 检查目标用户是否有该粉丝身份(RPC 调用 userService)
- 检查是否已是好友
- 检查历史请求记录(防骚扰机制):
- 调用
GetLatestRequest获取最近的请求 - 如果状态为
pending:返回"已发送过请求" - 如果状态为
rejected:检查processed_at是否过了冷却期(7天) - 如果状态为
accepted:返回"已经是好友" - 如果状态为
expired:允许发送新请求
- 调用
- 创建好友请求记录
-
实现
GetFriendRequests方法- 查询好友请求列表
- 支持分页
- 支持按状态筛选
-
实现
HandleFriendRequest方法- 验证请求存在且状态为 pending
- 如果是接受:
- 使用事务
- 更新请求状态
- 创建双向好友关系
- 更新 social 字段
- 如果是拒绝:
- 更新请求状态
-
实现
GetFriendList方法- 查询好友列表
- 支持分页
- 支持关键词搜索
- 关联查询用户信息
-
实现
DeleteFriend方法- 使用事务
- 删除双向好友关系
- 更新 social 字段
-
实现
SetFriendRemark方法- 更新备注名
步骤 2.3:创建 friendService 主程序
文件:backend/services/friendService/main.go
任务清单:
- 初始化日志
- 初始化数据库连接
- 运行数据库迁移
- 初始化 Repository
- 初始化 Service
- 注册 Dubbo 服务
- 启动服务
阶段 3:网关层(Gateway)
步骤 3.1:创建 DTO
文件:backend/gateway/dto/friend_dto.go
任务清单:
- 定义
FriendRequestDTO - 定义
FriendDTO - 定义各个响应 DTO
步骤 3.2:创建转换器
文件:backend/gateway/dto/friend_converter.go
任务清单:
- 实现 Proto → DTO 转换函数
步骤 3.3:创建 Controller
文件:backend/gateway/controller/friend_controller.go
任务清单:
- 实现
SendFriendRequest方法 - 实现
GetFriendRequests方法 - 实现
HandleFriendRequest方法 - 实现
GetFriendList方法 - 实现
DeleteFriend方法 - 实现
SetFriendRemark方法
步骤 3.4:配置路由
文件:backend/gateway/router/router.go
任务清单:
- 添加好友相关路由
阶段 4:测试与优化
步骤 4.1:单元测试
任务清单:
- Repository 层测试
- Service 层测试
步骤 4.2:集成测试
任务清单:
- 测试发送好友请求
- 测试接受好友请求
- 测试拒绝好友请求
- 测试删除好友
- 测试查询好友列表
- 测试边界情况
步骤 4.3:性能优化
任务清单:
- 添加数据库索引
- 优化 SQL 查询
- 添加缓存(可选)
7. 设计决策(已确认)✅
7.1 业务逻辑决策
决策 1:好友数量限制 ✅
问题:是否需要限制好友数量?
最终决策:暂不限制,后续引入规则表动态配置
理由:
- ✅ 初期不限制,简化实现
- ✅ 预留扩展性,后续可通过规则表根据用户等级、会员状态等动态限制
- ✅ 在配置文件中预留了
FriendLimitConfig结构体
实现要点:
- 在
friend_config.go中预留了好友数量限制的配置项 FriendLimit.Enabled = false(默认不启用)- 后续可通过规则表动态调整每个用户的好友数量上限
决策 2:重复请求策略 ⭐ 关键业务逻辑 ✅
问题:如果 A 的请求被 B 拒绝后,A 多久可以再次发送请求?
最终决策:7 天后可以再次发送
理由:
- ✅ 防止用户被重复骚扰
- ✅ 7 天是合理的冷却期(既不太短也不太长)
- ✅ 给用户二次机会(可能第一次拒绝是误操作)
实现要点(已在文档 3.1.1 节详细说明):
- 查询最近的请求记录(所有状态)
- 如果状态为
rejected,检查processed_at距今是否超过 7 天 - 未超过冷却期:返回 429 错误 + 剩余天数提示
- 已超过冷却期:允许发送新请求
配置位置:
friend_config.go→TimeConstraints.RejectionCooldownDays = 7- 便于后续迁移到规则表
决策 3:请求过期时间 ✅
问题:好友请求多久后自动过期?
最终决策:30 天
理由:
- ✅ 30 天是合理的等待期
- ✅ 避免请求列表堆积过多过期请求
- ✅ 给接收者足够的时间考虑
实现要点:
- 创建请求时,设置
expires_at = created_at + 30天 - 定时任务扫描过期请求,更新状态为
expired
配置位置:
friend_config.go→TimeConstraints.RequestExpiryDays = 30- 便于后续迁移到规则表
决策 4:删除好友通知 ✅
问题:删除好友时,是否通知对方?
最终决策:不通知对方(静默删除)
理由:
- ✅ 避免尴尬和不必要的冲突
- ✅ 符合大多数社交产品的设计习惯
- ✅ 对方在下次查看好友列表时会自然发现
实现要点:
- 删除好友时,只删除双向关系记录
- 不发送任何推送通知
- 不在对方界面显示"已解除好友关系"
决策 5:好友关系的数据保留 ✅
问题:删除好友后,是否保留历史关系记录?
最终决策:完全删除(物理删除)
理由:
- ✅ 简化数据模型,无需软删除字段
- ✅ 减少数据库存储压力
- ✅ 符合用户预期(删除就是删除)
实现要点:
- 使用
DELETE FROM friendships WHERE ... - 同时删除双向关系(A→B 和 B→A)
- 更新
fan_profiles表的social字段(好友数量 -1)
7.2 技术实现决策
决策 6:好友列表查询优化 ✅
问题:好友列表可能很大,如何优化查询性能?
最终决策:支持分页查询 + 添加数据库索引
实现方案:
- ✅ 分页查询:支持
page和page_size参数 - ✅ 数据库索引:添加以下索引优化查询
-- 1. 复合索引:user_id + star_id + status (用于查询某用户在某明星下的好友列表) CREATE INDEX idx_friendships_user_star_status ON friendships(user_id, star_id, status); -- 2. 复合索引:user_id + star_id + created_at (用于按时间排序) CREATE INDEX idx_friendships_user_star_created ON friendships(user_id, star_id, created_at DESC); -- 3. 覆盖索引:包含常用查询字段,避免回表 CREATE INDEX idx_friendships_list_query ON friendships(user_id, star_id, status, friend_id, created_at, remark); - ✅ 关键词搜索优化:
- 如果有
keyword参数,需要 JOINfan_profiles表查询昵称 - 添加索引:
CREATE INDEX idx_fan_profiles_nickname ON fan_profiles(nickname);
- 如果有
- ✅ 预加载用户信息:避免 N+1 查询
- 使用
LEFT JOIN一次性查询好友的用户信息 - 或使用
IN查询批量获取用户信息
- 使用
查询示例:
-- 基础查询(无关键词)
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 ?;
性能预期:
- 无关键词查询:使用覆盖索引,性能极佳
- 带关键词查询:使用
idx_fan_profiles_nickname索引,性能良好 - 分页查询:避免一次性加载大量数据
决策 7:并发控制 ✅
问题:如何处理并发请求的冲突?
最终决策:使用数据库事务保证原子性
实现方案:
- ✅ 唯一索引防止重复记录:
CONSTRAINT uk_friendships_user_friend_star UNIQUE (user_id, friend_id, star_id) - ✅ 使用数据库事务:
- 接受好友请求:事务包含(更新请求状态 + 创建双向好友关系 + 更新 social 字段)
- 删除好友:事务包含(删除双向好友关系 + 更新 social 字段)
- ✅ 事务隔离级别:使用
READ COMMITTED(PostgreSQL 默认) - ✅ 错误处理:
- 唯一索引冲突 → 返回"已经是好友"
- 事务失败 → 回滚所有操作,返回 500 错误
并发场景处理:
-
A 和 B 同时向对方发送请求:
- 允许两个请求都创建成功
- 任一方接受后,双方都成为好友
-
A 删除好友的同时,B 也在删除:
- 使用事务保证原子性
- 第一个事务成功,第二个事务会因为记录不存在而失败
- 返回友好的错误提示
决策 8:跨服务调用 ✅
问题:friendService 需要验证用户信息,如何调用 userService?
最终决策:通过 Dubbo RPC 调用
实现方案:
-
✅ 创建 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) } -
✅ 调用时机:
- 发送好友请求前:验证目标用户是否存在 + 是否有该粉丝身份
- 获取好友列表时:批量获取好友的用户信息
-
✅ 错误处理:
- RPC 调用失败 → 返回 500 错误
- 用户不存在 → 返回 404 错误
- 用户没有该粉丝身份 → 返回 400 错误
-
✅ 性能优化:
- 使用批量查询接口(
GetUsersByIDs)避免 N+1 问题 - 考虑添加本地缓存(Redis)缓存用户基本信息
- 使用批量查询接口(
7.3 扩展功能(可选)
功能 1:好友分组
描述:支持将好友分组管理(如"亲友"、"同城"等)
优先级:低
功能 2:好友推荐
描述:基于共同好友、兴趣等推荐可能认识的人
优先级:中
功能 3:屏蔽功能
描述:屏蔽某人后,对方无法向你发送好友请求
优先级:中
功能 4:亲密度系统
描述:根据互动频率计算好友亲密度
优先级:低
8. 附录
8.1 数据库脚本
创建 friendships 表
CREATE TABLE friendships (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
friend_id BIGINT NOT NULL,
star_id BIGINT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'accepted',
remark VARCHAR(50),
intimacy INT DEFAULT 0,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
CONSTRAINT fk_friendships_user
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_friendships_friend
FOREIGN KEY (friend_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_friendships_star
FOREIGN KEY (star_id) REFERENCES stars(star_id) ON DELETE CASCADE,
CONSTRAINT uk_friendships_user_friend_star
UNIQUE (user_id, friend_id, star_id),
CONSTRAINT chk_friendships_not_self
CHECK (user_id != friend_id)
);
-- 查询索引(优化好友列表查询性能)
CREATE INDEX idx_friendships_user_star_status ON friendships(user_id, star_id, status);
CREATE INDEX idx_friendships_user_star_created ON friendships(user_id, star_id, created_at DESC);
CREATE INDEX idx_friendships_list_query ON friendships(user_id, star_id, status, friend_id, created_at, remark);
CREATE INDEX idx_friendships_friend_star ON friendships(friend_id, star_id);
创建 friend_requests 表
CREATE TABLE friend_requests (
id BIGSERIAL PRIMARY KEY,
from_user_id BIGINT NOT NULL,
to_user_id BIGINT NOT NULL,
star_id BIGINT NOT NULL,
message VARCHAR(200),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
expires_at BIGINT,
processed_at BIGINT,
CONSTRAINT fk_friend_requests_from
FOREIGN KEY (from_user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_friend_requests_to
FOREIGN KEY (to_user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_friend_requests_star
FOREIGN KEY (star_id) REFERENCES stars(star_id) ON DELETE CASCADE,
CONSTRAINT chk_friend_requests_not_self
CHECK (from_user_id != to_user_id)
);
-- 查询索引(优化好友请求列表查询性能)
CREATE INDEX idx_friend_requests_to_status ON friend_requests(to_user_id, status, created_at DESC);
CREATE INDEX idx_friend_requests_from_status ON friend_requests(from_user_id, status, created_at DESC);
CREATE INDEX idx_friend_requests_star ON friend_requests(star_id);
CREATE INDEX idx_friend_requests_expires ON friend_requests(expires_at, status);
CREATE INDEX idx_friend_requests_users_star ON friend_requests(from_user_id, to_user_id, star_id, created_at DESC);
8.2 错误码定义
| 错误码 | 错误信息 | 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天后再试 |
9. 变更历史
| 版本 | 日期 | 作者 | 变更内容 |
|---|---|---|---|
| v0.1 | 2026-01-05 | System | 初始版本,包含基础设计 |
| v1.0 | 2026-01-06 | System | 确认所有设计决策,完善索引设计,创建配置文件 |
v1.0 详细变更:
- ✅ 确认问题1:暂不限制好友数量,后续引入规则表
- ✅ 确认问题2:7天冷却期,配置在
friend_config.go - ✅ 确认问题3:30天过期时间,配置在
friend_config.go - ✅ 确认问题4:删除好友不通知对方
- ✅ 确认问题5:完全删除好友关系
- ✅ 确认问题6:添加完整的数据库索引设计,支持分页查询
- ✅ 确认问题7:使用数据库事务保证原子性
- ✅ 确认问题8:通过 Dubbo RPC 调用 userService
- ✅ 创建
friend_config.go统一管理时间限制配置 - ✅ 优化数据库索引设计(friendships 表 4个索引,friend_requests 表 5个索引)
10. 后续工作
- 用户补充设计想法和修改意见 ✅
- 根据反馈修订设计方案 ✅
- 确定最终实现方案 ✅
- 开始编码实现(按照实现步骤章节执行)
📝 设计方案已确认,可以开始实现了! ✅