# 联系人、好友申请与个人群

好友关系、屏蔽关系、聊天可见性和群成员是四类独立状态。删除聊天不等于删除好友，群静音不等于屏蔽群成员，个人删除消息也不是全员撤回。本页给出这些状态的操作顺序、权限与恢复边界；全部业务请求使用聊天 Bearer 会话。

## 三种添加方式

`POST /friend-requests` 的 JSON 在下列三个字符串字段中恰选一个。不要同时发送空字符串的备用字段，因为服务端按字段是否为字符串计数。

| 字段 | 规范化与验证 | 额外条件 |
| --- | --- | --- |
| friendId | 删除空格、短横线并大写；11位字母数字 | 查找唯一聊天好友ID；不是Account用户ID |
| phone | 删除空格、短横线；大陆11位号码补+86；国际号码带+ | 只查登录时取得、尚未过期的已验证手机号摘要 |
| temporaryCode | 删除空格、短横线并大写；12位十六进制 | 对方用`POST /friend-codes`生成，10分钟有效 |

临时码按账号保存HMAC摘要，生成新码替换旧码；生成每用户每10分钟最多10次。手机号与临时码查找各自每10分钟最多20次，好友ID路径没有在该层调用同一计数器。不能由此推导全部联系人请求都有统一限流。依据：`chat-service/src/server.mjs:832-898`、`chat-service/src/database.mjs:371-400`。

临时码不是“成功成为好友才消费”：服务端找到有效码，排除添加自己和双方屏蔽后就删除该码，然后执行申请状态判断。因此同向pending、已是好友、存在反向申请，也可能已经消费临时码。重试前先刷新申请和会话列表，不应反复提交同一码。依据：`chat-service/src/database.mjs:500-509`。

## 申请与接受的完整顺序

1. 发送申请前展示目标添加方式，提交单一字段。
2. `201 {status:"requested",peer,requestId}`表示新建申请；当前还不是好友。接收者收到friend_request事件，刷新`GET /friend-requests`。
3. 同向申请已存在返回`200 {status:"pending",...}`，requestId可能为null，应从outgoing列表取得现有记录。
4. 对方已有反向申请返回409 INCOMING_REQUEST，转到incoming列表接受；已是好友为409 ALREADY_FRIENDS，任一方屏蔽为403 CONTACT_BLOCKED。
5. 接收者调用`POST /friend-requests/{requestId}/accept`。服务端在事务中验证接收者、删除申请、建立或恢复双方唯一会话，并返回conversationId。
6. 双方按friend_accepted事件重读会话。取消或拒绝统一用DELETE申请，双方才有权限，响应status=deleted。

重新成为好友会复用原有双方会话ID，不重新建立另一份单聊；原来的个人历史可见边界也不会因此自动重置。依据：`chat-service/src/database.mjs:476-564`。

合成成功例：`{"temporaryCode":"A1B2C3D4E5F6"}`对应一次申请，随后由对方按返回requestId接受。合成失败例：同时提交`{"friendId":"ABC123DEF45","phone":""}`会得到400 INVALID_INPUT，不能把空phone当作未传。票据、好友码和用户ID示例仅用于解释数据形状，不应拿真实用户资料排错。

## 手机号、资料与竞争更新

手机号添加使用Account最近登录返回的已验证号码摘要，当前聊天服务将可查找期限设为24小时；它与个人资料中自行填写、按private/selected/friends/everyone显示的contactPhone是不同字段。隐藏资料联系电话并不等同于关闭上述已验证号码查找路径，修改资料电话也不更新Account身份号码。界面应按实际字段解释行为，不能从一个开关推断另一个功能已经关闭。依据：`chat-service/src/database.mjs:279-287,324-375`。

申请列表只是快照：在用户点击接受前，对方可能取消申请，或任一方完成屏蔽。服务端事务重新校验并决定结果，客户端不能仅凭卡片曾显示就假定成功。接受请求若响应丢失，重试可能404，因为原申请已经删除；此时重读conversations确认双方关系。正常情况下，同一对用户始终复用唯一会话，客户端应按conversationId合并，而不是给每次接受创建重复条目。

## 屏蔽与删除好友

`PUT /blocks/{userId}`是有方向的本人屏蔽；业务检查会合并任一方向屏蔽。成功后清掉双方未完成申请及对应单聊的全部实时位置记录，写入block_changed事件，但不将is_friend改为false，也不删除已有聊天正文。`DELETE /blocks/{userId}`只解除本人的屏蔽，不恢复已删除的申请或位置共享，也不能解除对方对自己的屏蔽。

若存在该单聊通话，屏蔽路由先删除媒体房间并结束通话；媒体处理失败返回503，不能把界面按钮点击当作已完成屏蔽。已有MLS范围会被阻塞。依据：`chat-service/src/server.mjs:845-864`、`chat-service/src/database.mjs:672-725`。

`DELETE /friends/{conversationId}`中的路径参数是会话ID。它将is_friend改false、清理双方申请与单聊实时位置，并通知双方；历史消息与会话记录继续存在。恢复联系需重新申请并接受。非好友不能继续普通发送、开始或接听单聊通话、开始或更新单聊实时位置。

历史读取需要单独判断：普通聊天媒体的所有者可直接下载；非所有者凭仍可见的消息授权，受个人历史边界和单条隐藏过滤。单聊媒体查询不重新检查is_friend或block，因此屏蔽、删好友不保证旧的可见媒体立即失效。群消息授权分支要求当前群成员，但媒体所有者分支仍独立存在。删除本人消息也不会物理删除自己上传的文件。依据：`chat-service/src/database.mjs:239-254`。

## 个人群权限与成员历史

| 动作 | 请求字段 | 权限与结果 |
| --- | --- | --- |
| 创建群 | name为1–40字；memberIds为1–49个不同好友用户id（UUID），不含自己；不是11位friendId | 发起者成为群主；包含自己后最多50人 |
| 邀请成员 | memberIds为1–49个不同好友用户id（UUID），不是11位friendId | 仅群主；只把不在群中的对象当新增，总数最多50 |
| 改名 | name为1–40字 | 仅群主；产生group_renamed事件 |
| 移除成员 | 路径目标用户ID | 仅群主；不能移除自己，应走leave |
| 退出群 | 无业务字段 | 当前成员；群主退出会自动转移归属 |
| 解散群 | 无业务字段 | 仅群主；先处理进行中的通话，再清理群相关数据 |

创建时所有成员从新群开始；后续加入者的visible_after_message_id及last_read_message_id都设为加入当时最新群消息ID，所以不能阅读入群前历史。群主退出时，服务端按joined_at、user_id排序选最早剩余成员为新群主；最后成员退出删除群核心记录。leave与disband是不同清理分支，不能将最后成员退出描述成执行了完整解散清理。依据：`chat-service/src/database.mjs:764-777,950-1033`。

移除成员、退出与解散会处理相应通话及位置共享；移除或退出使当前MLS范围blocked。群主的个人身份与MLS创建者设备是不同角色，群主转移并不自动转移MLS创建者权限。企业组织和企业频道另见[工作台](https://document.thestarline.cn/app/workbench.md)，不是此处的个人群功能。

## 隐藏、删除与恢复

| 操作 | 服务端变化 | 能否恢复旧历史 |
| --- | --- | --- |
| DELETE某条messages/{messageId} | 给当前用户插入hidden_messages | 没有公开单条取消隐藏接口；其他人仍可见 |
| DELETE单聊 | 本人is_hidden=true，历史边界推进到当时最后消息 | restore只取消隐藏，不倒退边界 |
| DELETE个人群视图 | 本人隐藏、历史边界与已读位置都推进 | restore不恢复边界前消息，也不等于重新入群 |
| POST群mute | 只修改本人is_muted | 可再次设false；不改变成员关系、正文或已读 |

`includeHidden=1`用于获取自己仍有资格访问的隐藏会话或群。普通新消息会重新显示隐藏聊天，但旧历史边界仍有效。群静音更新没有在该方法中写入普通events，不能把它承诺为通过事件即时同步的跨设备设置。依据：`chat-service/src/database.mjs:635-652,730-760,855-880,904-925`。

## 已读与异常处理

单聊`/read`和群`/read`发送`{lastMessageId}`，位置只前进；目标消息必须属于当前聊天，群还要求消息晚于该成员历史边界。0虽然通过整数格式检查，但并不对应实际消息，仍会INVALID_MESSAGE。单聊用peerReadMessageId表示对方已读，群用其他成员已读位置最小值allReadMessageId；不能通过未读数算出逐人阅读时间。

正常例：连续提交同一有效lastMessageId均可返回status=read，第二次不重复推进；失败例：把单聊消息ID提交给群read得到400 INVALID_MESSAGE。收到404时先刷新当前成员和申请状态，403先处理权限与屏蔽，409 CLIENT_ID_REUSED不得更改内容后继续沿用原发送ID。完整参数见[API参考](https://document.thestarline.cn/app/api-reference.md)，错误与重试见[限制与错误](https://document.thestarline.cn/app/limits-and-errors.md)。本页依据源码阅读，未构成真实双端验收。

源码核对范围：2026-10-23 的当前实现。本轮未执行生产业务请求或原生设备验收。
