# Social Relations API

本文定义 App 端访问记录、关注、好友申请和好友关系接口。关系事实由 `user-service` 持久化；腾讯云 IM 只负责把好友申请消息送进聊天窗口，不作为好友关系事实来源。

## Core Flow

```mermaid
sequenceDiagram
  participant A as User A App
  participant G as gateway-service
  participant U as user-service
  participant IM as Tencent IM
  participant B as User B App

  A->>G: POST /api/v1/users/{B}/friend-requests
  G->>U: ApplyFriend(A, B)
  U-->>G: pending application
  G-->>A: application=pending
  A->>IM: send custom friend request message to B
  IM-->>B: chat window message
  B->>G: POST /api/v1/users/{A}/friend-requests/accept
  G->>U: AcceptFriendApplication(B, A)
  U-->>G: bidirectional friendship
  G-->>B: friend record
```

关键边界：

- 申请接口只创建 pending 申请，不直接发 IM。
- 同意必须调用 accept 接口，不能只靠 IM 消息状态。
- 删除好友是双向删除。
- 自己访问自己、关注自己、加自己好友全部拒绝。
- 重复访问只增加访问次数，不重复增加 `visitors_count`。
- VIP 匿名访问权益与用户开关同时生效时，本次访问完全不写访客事实、不刷新旧记录，也不增加 `visitors_count`；开启开关不会删除此前的公开访问历史。
- 重复关注、重复取消关注、重复同意、重复删除都按幂等处理。

## HTTP Endpoints

所有接口都要求完成资料的登录态，响应统一使用 `{code,message,request_id,data}` envelope。

| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/api/v1/users/{user_id}/visit` | 记录访问用户主页 |
| `GET` | `/api/v1/users/me/visitors?page=1&page_size=20` | 我的访问记录 |
| `POST` | `/api/v1/users/{user_id}/follow` | 关注用户 |
| `DELETE` | `/api/v1/users/{user_id}/follow` | 取消关注 |
| `GET` | `/api/v1/users/me/following?page=1&page_size=20` | 我的关注列表 |
| `POST` | `/api/v1/users/{user_id}/friend-requests` | 申请添加好友 |
| `POST` | `/api/v1/users/{user_id}/friend-requests/accept` | 同意该用户发来的好友申请 |
| `GET` | `/api/v1/users/me/friend-requests?direction=incoming&page=1&page_size=20` | 好友申请列表，`direction` 支持 `incoming`/`outgoing` |
| `GET` | `/api/v1/users/me/friends?page=1&page_size=20` | 我的好友列表 |
| `DELETE` | `/api/v1/users/{user_id}/friend` | 删除好友 |

## Response Examples

访问资料卡：

```json
{
  "recorded": true,
  "target_stats": {
    "visitors_count": 1,
    "following_count": 0,
    "followers_count": 1,
    "friends_count": 0,
    "updated_at_ms": 1777996800000
  },
  "target_profile": {
    "user_id": "10002",
    "display_user_id": "163002",
    "username": "Bob",
    "avatar": "https://cdn.example.com/avatar.png",
    "profile_bg_img": "https://cdn.example.com/profile-bg.png"
  }
}
```

匿名访问成功时 `recorded=false`，仍可返回目标资料与未隐藏的统计，但 `GET /api/v1/users/me/visitors` 不会出现本次访问。

申请好友：

```json
{
  "application": {
    "requester_user_id": "10001",
    "target_user_id": "10002",
    "status": "pending",
    "created_at_ms": 1777996800000,
    "updated_at_ms": 1777996800000
  },
  "already_friends": false
}
```

同意好友：

```json
{
  "friend": {
    "user_id": "10002",
    "friend_user_id": "10001",
    "friended_at_ms": 1777996810000
  }
}
```

关注：

```json
{
  "following": true,
  "follower_stats": {
    "visitors_count": 0,
    "following_count": 1,
    "followers_count": 0,
    "friends_count": 0,
    "updated_at_ms": 1777996800000
  }
}
```

## Storage

| Table | Owner | Purpose |
| --- | --- | --- |
| `user_profile_stats` | user-service | 我的页计数 read model：访问人数、关注数、粉丝数、好友数 |
| `user_profile_visits` | user-service | 用户主页访问去重记录，主键 `(app_code,target_user_id,visitor_user_id)` |
| `user_profile_anonymous_visits` | user-service | 只读兼容历史匿名记录；新匿名访问不再写入 |
| `user_follows` | user-service | 单向关注关系，取消关注只改状态 |
| `user_friend_applications` | user-service | 好友申请，pending/accepted |
| `user_friendships` | user-service | 双向好友边，每个好友关系写两行 |

计数更新和关系边写入必须在同一个 MySQL 事务中完成。关注时同时增加关注者的 `following_count` 和被关注者的 `followers_count`，取消关注时只在 active 状态真实变化时双边递减。`user_profile_stats` 是首屏和资料卡计数来源，不能在 HTTP 查询里实时扫关系表。

客态资料卡的 `target_stats` 同时返回 `followers_count`。历史 Fami 客户端曾把该回包的 `friends_count` 渲染为 Followers，因此 gateway 只在 `target_stats` 中保留 `friends_count=followers_count` 兼容别名；`/users/me/overview.profile_stats.friends_count` 始终保持真实好友数。

## Room Feed Integration

- `GET /api/v1/rooms/feeds?tab=visited` 是房间访问历史，只由 JoinRoom 成功后写入 room-service 的房间 feed 索引；不要使用 `/users/{user_id}/visit` 用户主页访问记录填充。
- `GET /api/v1/rooms/feeds?tab=following` 由 gateway 调用 `ListFollowing` 读取关注用户，再把关系用户和 `followed_at_ms` 传给 room-service 查询 active 房间卡片。
- `GET /api/v1/rooms/feeds?tab=friend` 由 gateway 调用 `ListFriends` 读取好友用户，再把关系用户和 `friended_at_ms` 传给 room-service 查询 active 房间卡片。
- `GET /api/v1/rooms/feeds?tab=followed` 是房间关注列表，只读取 room-service 的 `room_follows` 用户-房间关系，再 join active 房间卡片。
- 好友/用户关注关系事实仍归 user-service；房间关注关系事实归 room-service，不能用 user-service 的用户关注表替代。

## App Notes

- 好友申请成功后，App 发送腾讯 IM 自定义消息给对方；消息里携带 `requester_user_id` 和申请状态展示参数。
- 聊天窗口点击同意时调用 `/api/v1/users/{requester_user_id}/friend-requests/accept`。
- 如果 accept 返回成功，App 再刷新好友列表或本地会话状态。
- 如果 IM 消息丢失，pending 申请仍可在 `/api/v1/users/me/friend-requests?direction=incoming` 找回。
