This commit is contained in:
oneao committed 2026-06-25 16:47:54 +08:00
1 parent 513a991b57
commit 8f8e8d03c8
22 files changed
+2212 -213

No files matched your search

+245
View File
@@ -0,0 +1,245 @@
# 家庭聊天模块
## 概述
一个家庭一个聊天室,成员通过 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` 是否存在即可)
- 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 筛选
- 按日期范围筛选
- 点击消息跳回聊天室定位