7.9 KiB
7.9 KiB
家庭聊天模块
概述
一个家庭一个聊天室,成员通过 WebSocket 实时收发消息。支持模块内容转发、图片/视频/文件、消息撤回与删除。
数据库表
b_chat — 聊天消息
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 — 通用附件
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 等模块共用。
存量表改动
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。
客户端消息:
// 发送文本
{"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}
服务端广播:
// 新消息
{"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,无需额外安装。重连逻辑在 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 筛选
- 按日期范围筛选
- 点击消息跳回聊天室定位