Files
workspace/code/app/CHAT_MODULE.md
T
2026-06-25 17:31:07 +08:00

246 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 家庭聊天模块
## 概述
一个家庭一个聊天室,成员通过 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 筛选
- 按日期范围筛选
- 点击消息跳回聊天室定位