Files
workspace/code/app/CHAT_MODULE.md
T
2026-06-25 16:47:54 +08:00

7.8 KiB
Raw Blame History

家庭聊天模块

概述

一个家庭一个聊天室,成员通过 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 是否存在即可)
  • 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 筛选
  • 按日期范围筛选
  • 点击消息跳回聊天室定位