246 lines
7.9 KiB
Markdown
246 lines
7.9 KiB
Markdown
# 家庭聊天模块
|
||
|
||
## 概述
|
||
|
||
一个家庭一个聊天室,成员通过 WebSocket 实时收发消息。支持模块内容转发、图片/视频/文件、消息撤回与删除。
|
||
|
||
---
|
||
|
||
## 数据库表
|
||
|
||
### b_chat — 聊天消息
|
||
|
||
```sql
|
||
CREATE TABLE b_chat (
|
||
id BIGINT PRIMARY KEY,
|
||
family_id BIGINT NOT NULL,
|
||
user_id BIGINT NOT NULL,
|
||
content TEXT NOT NULL DEFAULT '',
|
||
type SMALLINT NOT NULL DEFAULT 0, -- 0=text 1=image 2=video 3=file 4=source
|
||
source_table VARCHAR(64),
|
||
source_id BIGINT,
|
||
source_data TEXT, -- 转发时存的快照 JSON
|
||
recalled SMALLINT NOT NULL DEFAULT 0, -- 0=否 1=撤回
|
||
deleted SMALLINT NOT NULL DEFAULT 0, -- 0=否 1=管理员删除
|
||
create_time TIMESTAMPTZ NOT NULL,
|
||
update_time TIMESTAMPTZ NOT NULL,
|
||
create_by BIGINT NOT NULL,
|
||
update_by BIGINT NOT NULL
|
||
);
|
||
```
|
||
|
||
**为什么不存 nickname / avatar**
|
||
|
||
消息只存 `user_id`,昵称和头像通过 `b_family_member` JOIN 实时取。这样用户更新资料后,所有历史消息自动显示最新的昵称和头像。如果成员已退出家庭(JOIN 不到),前端显示"已退出的成员"兜底。
|
||
|
||
**字段说明**
|
||
|
||
- type: `0` 文本 / `1` 图片 / `2` 视频 / `3` 文件 / `4` 转发(所有子模块均可转发,服务端校验 `source_table` 是否存在即可) / `5` 系统消息(加入家庭、改名、记账、收藏等,替代 `b_family_feed`)
|
||
- recalled: 发送者 **2 分钟内**可撤回,全员看到"xxx 撤回了一条消息"
|
||
- deleted: `b_family_member.role = 0`(管理员)可删除任意消息,全员不可见
|
||
|
||
### b_file — 通用附件
|
||
|
||
```sql
|
||
CREATE TABLE b_file (
|
||
id BIGINT PRIMARY KEY,
|
||
target_id BIGINT NOT NULL,
|
||
file_key VARCHAR(256) NOT NULL,
|
||
file_name VARCHAR(256),
|
||
file_type SMALLINT NOT NULL, -- 1=image 2=video 3=file
|
||
mime_type VARCHAR(64),
|
||
file_size BIGINT,
|
||
width INT,
|
||
height INT,
|
||
duration INT,
|
||
sort_order INT NOT NULL DEFAULT 0,
|
||
create_time TIMESTAMPTZ NOT NULL,
|
||
update_time TIMESTAMPTZ NOT NULL,
|
||
create_by BIGINT NOT NULL,
|
||
update_by BIGINT NOT NULL
|
||
);
|
||
```
|
||
|
||
`target_id` 关联到任意表的记录(雪花 ID 全局唯一),chat / note / task 等模块共用。
|
||
|
||
### 存量表改动
|
||
|
||
```sql
|
||
ALTER TABLE b_family_member ADD COLUMN last_read_id BIGINT DEFAULT 0;
|
||
```
|
||
|
||
---
|
||
|
||
## 后端实现
|
||
|
||
### 文件结构
|
||
|
||
```
|
||
app-go/internal/
|
||
├── ws/
|
||
│ ├── hub.go -- Hub + Room(map[family_id]*Room)
|
||
│ ├── client.go -- 单连接读写 goroutine、心跳、断连处理
|
||
│ └── handler.go -- HTTP → WS 升级、鉴权、消息路由
|
||
└── router/
|
||
└── router.go -- 新增 GET /app/chat/ws
|
||
```
|
||
|
||
依赖:`github.com/gofiber/contrib/v3/websocket`(Fiber v3 官方 WS 支持)
|
||
|
||
### WebSocket 协议
|
||
|
||
连接:`GET /app/chat/ws?token=xxx&family_id=123`
|
||
|
||
鉴权独立实现(不复用 `middleware.Auth()`,WS 无法设自定义 header):token 解 user_id → family_id 校验归属 → 加入 Room。
|
||
|
||
客户端消息:
|
||
|
||
```jsonc
|
||
// 发送文本
|
||
{"type": "send", "content": "晚上吃啥"}
|
||
|
||
// 发送图片/视频/文件(先 S3 上传拿 key,再发消息)
|
||
{"type": "send", "content": "", "msg_type": 1, "attachments": [
|
||
{"file_key": "2026/06/25/uuid.jpg", "file_name": "photo.jpg", "file_type": 1, "mime_type": "image/jpeg", "file_size": 204800, "width": 1080, "height": 720}
|
||
]}
|
||
|
||
// 转发
|
||
{"type": "send", "content": "看看这笔", "msg_type": 4, "source_table": "b_finance", "source_id": 456}
|
||
|
||
// 撤回
|
||
{"type": "recall", "message_id": 123}
|
||
|
||
// 删除(管理员)
|
||
{"type": "delete", "message_id": 123}
|
||
```
|
||
|
||
服务端广播:
|
||
|
||
```jsonc
|
||
// 新消息
|
||
{"type": "message", "data": {
|
||
"id": 123, "family_id": 1, "user_id": 1,
|
||
"nickname": "小明", "avatar": "https://...", // 从 b_family_member JOIN
|
||
"content": "晚上吃啥", "msg_type": 0,
|
||
"attachments": null,
|
||
"source_table": null, "source_id": null, "source_data": null,
|
||
"create_time": "2026-06-25 21:30:00"
|
||
}}
|
||
|
||
// 撤回通知(全员)
|
||
{"type": "message_recalled", "message_id": 123}
|
||
|
||
// 删除通知(全员)
|
||
{"type": "message_deleted", "message_id": 123}
|
||
|
||
// 在线成员(全员广播,全量替换式)
|
||
{"type": "online", "users": [{"user_id": 1, "nickname": "小明", "avatar": "https://..."}]}
|
||
|
||
// 成员下线(单条)
|
||
{"type": "offline", "user_id": 3}
|
||
```
|
||
|
||
### 消息处理流程
|
||
|
||
```
|
||
客户端 WS 消息
|
||
→ 解析 type 字段路由:
|
||
|
||
send:
|
||
1. 从连接上下文取 family_id / user_id
|
||
2. getUniqueId 拿消息 ID
|
||
3. 若 msg_type = 4(转发):查 source_table WHERE id = source_id,整行转 JSON 存 source_data
|
||
4. 若 attachments 非空:批量 INSERT b_file(target_id = 消息 ID)
|
||
5. INSERT b_chat
|
||
6. JOIN b_family_member 取 nickname / avatar
|
||
7. 组装 message 广播到 Room
|
||
|
||
recall:
|
||
1. 查 b_chat WHERE id = message_id
|
||
2. 校验 user_id == 当前用户 && now - create_time <= 2 分钟
|
||
3. UPDATE b_chat SET recalled = 1
|
||
4. 广播 message_recalled
|
||
|
||
delete:
|
||
1. 查 b_family_member WHERE family_id = ? AND user_id = 当前用户
|
||
2. 校验 role = 0
|
||
3. UPDATE b_chat SET deleted = 1
|
||
4. 广播 message_deleted
|
||
```
|
||
|
||
在线状态:WS 连上时广播 `online`(全量),断开时广播 `offline`(单条)。
|
||
|
||
---
|
||
|
||
## 前端实现
|
||
|
||
### 文件结构
|
||
|
||
```
|
||
app-rn/src/
|
||
├── app/(main)/(tabs)/chat/
|
||
│ └── index.tsx -- 聊天室
|
||
├── app/(main)/chat/
|
||
│ └── history.tsx -- 历史消息搜索
|
||
├── components/chat/
|
||
│ ├── MessageList.tsx -- FlatList 倒序渲染
|
||
│ ├── TextBubble.tsx -- 文本气泡
|
||
│ ├── ImageBubble.tsx -- 图片气泡
|
||
│ ├── VideoBubble.tsx -- 视频气泡
|
||
│ ├── FileBubble.tsx -- 文件气泡
|
||
│ ├── ForwardCard.tsx -- 转发卡片(source_data 渲染)
|
||
│ ├── InputBar.tsx -- 输入框 + 附件按钮
|
||
│ └── OnlineBar.tsx -- 在线成员头像行
|
||
└── hooks/
|
||
└── useChatWebSocket.ts -- WS 连接、自动重连、消息去重
|
||
```
|
||
|
||
### 聊天页面数据流
|
||
|
||
```
|
||
进入页面
|
||
→ loadDataPageApi("b_chat", "family_id = ? AND deleted = 0", "create_time DESC", [familyId])
|
||
→ 取 b_family_member 获取成员昵称/头像 Map(发消息和渲染时用)
|
||
→ 若 msg_type IN (1,2,3):批量查 b_file WHERE target_id IN (...)
|
||
→ 连接 WS
|
||
|
||
收到 WS message
|
||
→ id 去重
|
||
→ 追加到 FlatList 底部
|
||
→ 更新 last_read_id
|
||
|
||
发送消息
|
||
→ 文本:直接 WS send
|
||
→ 图片/视频/文件:先 s3UploadFileApi → 拿 key → WS send
|
||
→ 转发:子模块调 WS send 带 source_table + source_id
|
||
→ 乐观更新(灰色气泡 → 成功实色 / 失败红色+重试)
|
||
|
||
撤回
|
||
→ WS send recall
|
||
→ 收到 message_recalled → 对应气泡替换为提示
|
||
|
||
删除(管理员可见删除按钮)
|
||
→ WS send delete
|
||
→ 收到 message_deleted → 移除该消息
|
||
```
|
||
|
||
依赖:React Native 内置 [`WebSocket`](https://reactnative.dev/docs/network#websocket),无需额外安装。重连逻辑在 `useChatWebSocket` hook 里封装。
|
||
|
||
### WS 连接管理
|
||
|
||
- 断线自动重连(exponential backoff:1s → 2s → 4s → … → 上限 30s)
|
||
- 重连后用 `loadDataPageApi` 拉 `WHERE id > lastReceivedId` 补洞
|
||
- `online` 事件更新 OnlineBar,`offline` 移除对应头像
|
||
|
||
### 转发卡片渲染
|
||
|
||
`JSON.parse(source_data)` 展示关键字段,点击跳转对应模块详情页。源记录删除后卡片仍可展示(快照自包含)。
|
||
|
||
### 历史消息搜索
|
||
|
||
独立页面 `chat/history.tsx`,走 `loadDataPageApi` + `loadDataBySqlApi`:
|
||
- 关键词搜索:`WHERE content ILIKE '%keyword%'`
|
||
- 按 msg_type 筛选
|
||
- 按日期范围筛选
|
||
- 点击消息跳回聊天室定位
|