# 好友功能设计方案(已确认版) > **文档状态**:设计已确认 ✅ > **创建时间**:2026-01-05 > **确认时间**:2026-01-06 > **目标**:定义好友系统的数据库设计、API 接口和业务流程 --- ## 📋 目录 - [1. 功能概述](#1-功能概述) - [2. 数据库设计](#2-数据库设计) - [3. 业务流程设计](#3-业务流程设计) - [4. API 接口设计](#4-api-接口设计) - [5. 状态机设计](#5-状态机设计) - [6. 实现步骤](#6-实现步骤) - [7. 待讨论问题](#7-待讨论问题) --- ## 1. 功能概述 ### 1.1 核心功能 - ✅ **发送好友请求**:用户 A 向用户 B 发送好友请求 - ✅ **接受好友请求**:用户 B 接受用户 A 的好友请求 - ✅ **拒绝好友请求**:用户 B 拒绝用户 A 的好友请求 - ✅ **删除好友**:用户可以删除已添加的好友 - ✅ **查看好友列表**:查看自己的好友列表 - ✅ **查看好友请求列表**:查看收到的好友请求 ### 1.2 设计原则 1. **粉丝身份隔离**:好友关系基于粉丝身份(star_id) - 用户在不同明星的身份下可以有不同的好友圈 - 例如:用户 A 作为"肖战粉丝"的好友 ≠ 作为"王一博粉丝"的好友 2. **双向关系**:好友关系是双向的 - A 添加 B 为好友,B 也会自动成为 A 的好友 - 删除好友时,双方都会失去好友关系 3. **请求-确认机制**: - A 发送请求 → B 收到请求 → B 接受/拒绝 → 建立/不建立好友关系 4. **数据一致性**: - 使用外键约束保证数据完整性 - 使用事务保证操作原子性 --- ## 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 | 更新时间(毫秒时间戳)| **索引设计**: ```sql -- 复合唯一索引(防止重复添加) 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) ``` **外键约束**: ```sql -- 用户外键 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 ``` **检查约束**: ```sql -- 不能添加自己为好友 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 | 处理时间(毫秒时间戳)| **索引设计**: ```sql -- 查询索引(优化好友请求列表查询性能) -- 索引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) ``` **外键约束**: ```sql -- 发送者外键 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 ``` **检查约束**: ```sql -- 不能请求自己为好友 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 的好友请求" │ │ │ │ ``` **关键验证**: 1. ✅ 不能添加自己为好友 2. ✅ 目标用户必须存在 3. ✅ 目标用户必须有该粉丝身份 4. ✅ 检查是否已经是好友 5. ✅ **检查历史请求记录(防骚扰机制)**: - 查询最近的请求记录(所有状态) - 如果有 `pending` 请求 → 提示"已发送过请求" - 如果有 `rejected` 请求 → 检查是否过了冷却期(7天) - 如果有 `accepted` 请求 → 提示"已经是好友" **异常处理**: - 目标用户不存在 → 返回 404 "用户不存在" - 目标用户没有该粉丝身份 → 返回 400 "对方没有该粉丝身份" - 已经是好友 → 返回 400 "已经是好友" - 已有未处理的请求 → 返回 400 "已发送过好友请求,请等待对方处理" - 请求被拒绝且在冷却期内 → 返回 400 "请求已被拒绝,请 X 天后再试" --- ### 3.1.1 防骚扰机制详解 **问题**:如何防止用户被重复骚扰? **解决方案**:基于请求历史的冷却期机制 #### 检查逻辑流程图 ``` 发送好友请求 ↓ 查询最近的请求记录(同 star_id 下) ↓ 是否有历史请求? ├─ 否 → ✅ 允许发送 └─ 是 ↓ 检查请求状态 ├─ pending → ❌ "已发送过请求,请等待对方处理" ├─ accepted → ❌ "已经是好友" ├─ expired → ✅ 允许发送新请求 └─ rejected ↓ 计算距离 processed_at 的时间 ├─ < 7天 → ❌ "请求已被拒绝,请 X 天后再试" └─ ≥ 7天 → ✅ 允许发送新请求 ``` #### 实现代码示例 ```go 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` 中: ```go // FriendConfig 好友功能配置 type FriendConfig struct { TimeConstraints TimeConstraints // 时间限制配置 FriendLimit FriendLimitConfig // 好友数量限制(预留) } // TimeConstraints 时间约束配置 type TimeConstraints struct { RejectionCooldownDays int // 被拒绝后的冷却期(天数) = 7天 RequestExpiryDays int // 好友请求的过期时间(天数) = 30天 RejectionCooldownMillis int64 // 被拒绝后的冷却期(毫秒) RequestExpiryMillis int64 // 好友请求的过期时间(毫秒) } ``` **配置说明**: - ✅ 统一管理所有时间限制,便于维护 - ✅ 便于后续迁移到规则表进行动态配置 - ✅ 提供了丰富的辅助方法(计算剩余天数、检查是否过期等) **使用示例**: ```go // 初始化配置 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 接受了您的好友请求" │ │ │ │ │ │ │──⑨ 返回成功─────────────→│ │ │ "已添加为好友" │ │ │ │ ``` **关键操作**: 1. ✅ 使用事务保证原子性 2. ✅ 创建双向好友关系(A→B 和 B→A) 3. ✅ 更新 fan_profiles 表的 social 字段(好友数量) 4. ✅ 更新请求状态为 accepted **异常处理**: - 请求不存在 → 返回 404 "好友请求不存在" - 请求已被处理 → 返回 400 "该请求已被处理" - 请求已过期 → 返回 400 "该请求已过期" - 事务失败 → 回滚所有操作,返回 500 "操作失败" --- ### 3.3 拒绝好友请求流程 ``` ┌─────────────────────────────────────────────────────────────┐ │ 拒绝好友请求 │ └─────────────────────────────────────────────────────────────┘ 用户 A 系统 用户 B │ │ │ │ │←─① 点击"拒绝"──────────────│ │ │ │ │ │──② 验证请求────────────────│ │ │ - 请求是否存在? │ │ │ - 请求状态是否为 pending? │ │ │ │ │ │──③ 更新请求状态────────────│ │ │ UPDATE friend_requests │ │ │ status = 'rejected' │ │ │ processed_at = now() │ │ │ │ │ │──④ 返回成功─────────────→│ │ │ "已拒绝好友请求" │ │ │ │ ``` **关键操作**: 1. ✅ 只更新请求状态,不创建好友关系 2. ✅ 不通知请求发送者(避免尴尬) 3. ✅ 保留拒绝记录(可用于防骚扰) --- ### 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 已将您从好友列表移除" │ │ │ │ ``` **关键操作**: 1. ✅ 使用事务保证原子性 2. ✅ 删除双向好友关系(A→B 和 B→A) 3. ✅ 更新 fan_profiles 表的 social 字段(减少好友数量) 4. ✅ 可选通知对方(根据产品需求决定) **异常处理**: - 好友关系不存在 → 返回 404 "好友关系不存在" - 事务失败 → 回滚所有操作,返回 500 "操作失败" --- ## 4. API 接口设计 ### 4.1 发送好友请求 **接口**:`POST /api/v1/friends/requests` **请求头**: ```http Authorization: Bearer Content-Type: application/json ``` **请求体**: ```json { "friend_user_id": 123, "message": "你好,我是你的粉丝,想和你成为好友" // 可选 } ``` **成功响应**(200): ```json { "code": 200, "message": "ok", "data": { "request_id": 456, "status": "pending", "created_at": 1704441600000 } } ``` **错误响应**: ```json // 400 - 已经是好友 { "code": 400, "message": "已经是好友" } // 400 - 已发送过请求 { "code": 400, "message": "已发送过好友请求,请等待对方处理" } // 404 - 用户不存在 { "code": 404, "message": "用户不存在" } ``` --- ### 4.2 获取好友请求列表 **接口**:`GET /api/v1/friends/requests` **请求头**: ```http Authorization: Bearer ``` **查询参数**: ``` type=received // 收到的请求(默认)或 sent(发出的请求) status=pending // 请求状态(可选):pending, accepted, rejected, all page=1 // 页码(可选,默认 1) page_size=20 // 每页数量(可选,默认 20) ``` **成功响应**(200): ```json { "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` **请求头**: ```http Authorization: Bearer Content-Type: application/json ``` **请求体**: ```json { "action": "accept" // accept(接受)或 reject(拒绝) } ``` **成功响应**(200): ```json { "code": 200, "message": "ok", "data": { "action": "accept", "friendship_created": true // 仅在 accept 时有此字段 } } ``` **错误响应**: ```json // 404 - 请求不存在 { "code": 404, "message": "好友请求不存在" } // 400 - 请求已被处理 { "code": 400, "message": "该请求已被处理" } ``` --- ### 4.4 获取好友列表 **接口**:`GET /api/v1/friends` **请求头**: ```http Authorization: Bearer ``` **查询参数**: ``` keyword=小明 // 搜索关键词(可选) page=1 // 页码(可选,默认 1) page_size=20 // 每页数量(可选,默认 20) ``` **成功响应**(200): ```json { "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}` **请求头**: ```http Authorization: Bearer ``` **成功响应**(200): ```json { "code": 200, "message": "ok", "data": {} } ``` **错误响应**: ```json // 404 - 好友关系不存在 { "code": 404, "message": "好友关系不存在" } ``` --- ### 4.6 设置好友备注 **接口**:`PUT /api/v1/friends/{user_id}/remark` **请求头**: ```http Authorization: Bearer Content-Type: application/json ``` **请求体**: ```json { "remark": "我的好朋友" // 最大 50 个字符 } ``` **成功响应**(200): ```json { "code": 200, "message": "ok", "data": { "remark": "我的好朋友" } } ``` --- ## 5. 状态机设计 ### 5.1 好友请求状态转换 ``` ┌─────────────┐ │ │ 创建请求 │ pending │ ────────→│ (待处理) │ │ │ └─────┬───┬───┘ │ │ 接受 │ │ 拒绝 ↓ │ │ ↓ ┌──────────┐ │ │ ┌──────────┐ │ │ │ │ │ │ │ accepted │ │ │ │ rejected │ │ (已接受) │ │ │ │ (已拒绝) │ │ │ │ │ │ │ └──────────┘ │ │ └──────────┘ │ │ │ │ 30天后 │ └────────→ │ ┌──────────┐ │ │ │ └────────→│ expired │ │ (已过期) │ │ │ └──────────┘ ``` **状态说明**: - `pending`:待处理(初始状态) - `accepted`:已接受(终态,好友关系已建立) - `rejected`:已拒绝(终态) - `expired`:已过期(终态,通过定时任务自动更新) **状态转换规则**: 1. ✅ `pending` → `accepted`:接受好友请求 2. ✅ `pending` → `rejected`:拒绝好友请求 3. ✅ `pending` → `expired`:30 天未处理自动过期 4. ❌ 终态不能再转换 --- ### 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` **接口定义**: ```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` **接口定义**: ```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 节详细说明): 1. 查询最近的请求记录(所有状态) 2. 如果状态为 `rejected`,检查 `processed_at` 距今是否超过 7 天 3. 未超过冷却期:返回 429 错误 + 剩余天数提示 4. 已超过冷却期:允许发送新请求 **配置位置**: - `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` 参数 - ✅ **数据库索引**:添加以下索引优化查询 ```sql -- 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` 参数,需要 JOIN `fan_profiles` 表查询昵称 - 添加索引:`CREATE INDEX idx_fan_profiles_nickname ON fan_profiles(nickname);` - ✅ **预加载用户信息**:避免 N+1 查询 - 使用 `LEFT JOIN` 一次性查询好友的用户信息 - 或使用 `IN` 查询批量获取用户信息 **查询示例**: ```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 ?; -- 带关键词搜索 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:并发控制 ✅ **问题**:如何处理并发请求的冲突? **最终决策**:**使用数据库事务保证原子性** **实现方案**: - ✅ **唯一索引防止重复记录**: ```sql CONSTRAINT uk_friendships_user_friend_star UNIQUE (user_id, friend_id, star_id) ``` - ✅ **使用数据库事务**: - 接受好友请求:事务包含(更新请求状态 + 创建双向好友关系 + 更新 social 字段) - 删除好友:事务包含(删除双向好友关系 + 更新 social 字段) - ✅ **事务隔离级别**:使用 `READ COMMITTED`(PostgreSQL 默认) - ✅ **错误处理**: - 唯一索引冲突 → 返回"已经是好友" - 事务失败 → 回滚所有操作,返回 500 错误 **并发场景处理**: 1. **A 和 B 同时向对方发送请求**: - 允许两个请求都创建成功 - 任一方接受后,双方都成为好友 2. **A 删除好友的同时,B 也在删除**: - 使用事务保证原子性 - 第一个事务成功,第二个事务会因为记录不存在而失败 - 返回友好的错误提示 --- #### 决策 8:跨服务调用 ✅ **问题**:friendService 需要验证用户信息,如何调用 userService? **最终决策**:**通过 Dubbo RPC 调用** **实现方案**: - ✅ **创建 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) } ``` - ✅ **调用时机**: - 发送好友请求前:验证目标用户是否存在 + 是否有该粉丝身份 - 获取好友列表时:批量获取好友的用户信息 - ✅ **错误处理**: - RPC 调用失败 → 返回 500 错误 - 用户不存在 → 返回 404 错误 - 用户没有该粉丝身份 → 返回 400 错误 - ✅ **性能优化**: - 使用批量查询接口(`GetUsersByIDs`)避免 N+1 问题 - 考虑添加本地缓存(Redis)缓存用户基本信息 --- ### 7.3 扩展功能(可选) #### 功能 1:好友分组 **描述**:支持将好友分组管理(如"亲友"、"同城"等) **优先级**:低 --- #### 功能 2:好友推荐 **描述**:基于共同好友、兴趣等推荐可能认识的人 **优先级**:中 --- #### 功能 3:屏蔽功能 **描述**:屏蔽某人后,对方无法向你发送好友请求 **优先级**:中 --- #### 功能 4:亲密度系统 **描述**:根据互动频率计算好友亲密度 **优先级**:低 --- ## 8. 附录 ### 8.1 数据库脚本 #### 创建 friendships 表 ```sql 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 表 ```sql 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. 后续工作 - [x] 用户补充设计想法和修改意见 ✅ - [x] 根据反馈修订设计方案 ✅ - [x] 确定最终实现方案 ✅ - [ ] 开始编码实现(按照实现步骤章节执行) --- **📝 设计方案已确认,可以开始实现了!** ✅ ---