topfans/backend/docs/好友功能设计方案.md
2026-04-07 22:29:48 +08:00

1424 lines
47 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 好友功能设计方案(已确认版)
> **文档状态**:设计已确认 ✅
> **创建时间**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暂不限制好友数量后续引入规则表
- ✅ 确认问题27天冷却期配置在 `friend_config.go`
- ✅ 确认问题330天过期时间配置在 `friend_config.go`
- ✅ 确认问题4删除好友不通知对方
- ✅ 确认问题5完全删除好友关系
- ✅ 确认问题6添加完整的数据库索引设计支持分页查询
- ✅ 确认问题7使用数据库事务保证原子性
- ✅ 确认问题8通过 Dubbo RPC 调用 userService
- ✅ 创建 `friend_config.go` 统一管理时间限制配置
- ✅ 优化数据库索引设计friendships 表 4个索引friend_requests 表 5个索引
---
## 10. 后续工作
- [x] 用户补充设计想法和修改意见 ✅
- [x] 根据反馈修订设计方案 ✅
- [x] 确定最终实现方案 ✅
- [ ] 开始编码实现(按照实现步骤章节执行)
---
**📝 设计方案已确认,可以开始实现了!** ✅
---