1424 lines
47 KiB
Markdown
1424 lines
47 KiB
Markdown
# 好友功能设计方案(已确认版)
|
||
|
||
> **文档状态**:设计已确认 ✅
|
||
> **创建时间**: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 <access_token>
|
||
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 <access_token>
|
||
```
|
||
|
||
**查询参数**:
|
||
```
|
||
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 <access_token>
|
||
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 <access_token>
|
||
```
|
||
|
||
**查询参数**:
|
||
```
|
||
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 <access_token>
|
||
```
|
||
|
||
**成功响应**(200):
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "ok",
|
||
"data": {}
|
||
}
|
||
```
|
||
|
||
**错误响应**:
|
||
```json
|
||
// 404 - 好友关系不存在
|
||
{
|
||
"code": 404,
|
||
"message": "好友关系不存在"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4.6 设置好友备注
|
||
|
||
**接口**:`PUT /api/v1/friends/{user_id}/remark`
|
||
|
||
**请求头**:
|
||
```http
|
||
Authorization: Bearer <access_token>
|
||
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] 确定最终实现方案 ✅
|
||
- [ ] 开始编码实现(按照实现步骤章节执行)
|
||
|
||
---
|
||
|
||
**📝 设计方案已确认,可以开始实现了!** ✅
|
||
|
||
---
|
||
|