数据库表创建-消息
版本归属
用于 v3 消息与历史记录版。
表清单
messages
v3 同时开始维护以下会话字段:
conversations.last_message_idconversations.last_message_atconversation_members.last_read_message_idconversation_members.last_read_at
messages
消息表。所有单聊和群聊消息统一存储在这里。
执行 SQL
sql
CREATE TABLE IF NOT EXISTS messages (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
conversation_id BIGINT NOT NULL,
sender_id BIGINT NOT NULL,
client_msg_id VARCHAR(64) NOT NULL,
message_type ENUM('text', 'image', 'file', 'system') NOT NULL,
content JSON NOT NULL,
status ENUM('normal', 'recalled') NOT NULL DEFAULT 'normal',
sent_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_messages_sender_client_msg (sender_id, client_msg_id),
KEY idx_messages_conversation_id_id (conversation_id, id),
KEY idx_messages_sent_at (sent_at)
);字段说明:
id:消息 ID,也是历史消息分页游标conversation_id:所属会话 IDsender_id:发送者用户 IDclient_msg_id:客户端生成的消息 ID,用于断线重试幂等message_type:消息类型,支持text、image、file、systemcontent:消息内容 JSON,不同消息类型使用不同结构status:消息状态,normal表示正常展示,recalled表示已软撤回sent_at:服务端确认的发送时间created_at:记录创建时间
索引说明:
uk_messages_sender_client_msg:保证同一发送者重复提交同一个client_msg_id时只生成一条消息idx_messages_conversation_id_id:用于按会话分页拉取历史消息idx_messages_sent_at:用于按时间查询消息
消息内容示例:
文本消息:
json
{
"text": "hello"
}图片消息:
json
{
"url": "https://example.com/a.png",
"width": 800,
"height": 600
}文件消息:
json
{
"url": "https://example.com/a.pdf",
"name": "a.pdf",
"size": 12345
}业务约束:
- 发送消息前必须校验发送者是会话
active成员 - 写入
messages和更新conversations.last_message_id、conversations.last_message_at必须在同一个事务中完成 - 重复提交同一
(sender_id, client_msg_id)时返回已有消息,不重复入库 - v3 使用
conversation_members.last_read_message_id计算会话级未读数 - 精细到单条消息的已读回执不在 v3 落地
- 撤回消息只更新
status = 'recalled',不删除原始content
已有表升级到支持撤回状态:
sql
ALTER TABLE messages
MODIFY status ENUM('normal', 'recalled') NOT NULL DEFAULT 'normal';建表后检查
sql
DESC messages;
SHOW INDEX FROM messages;最小验证
以下 SQL 假设已经存在 conversation_id = 1,且 user_id = 1 是该会话 active 成员。
插入文本消息:
sql
INSERT INTO messages (conversation_id, sender_id, client_msg_id, message_type, content)
VALUES (1, 1, 'client-001', 'text', JSON_OBJECT('text', 'hello'));更新会话最后消息:
sql
UPDATE conversations
SET last_message_id = LAST_INSERT_ID(), last_message_at = NOW()
WHERE id = 1;分页查询历史消息:
sql
SELECT id, conversation_id, sender_id, message_type, content, status, sent_at
FROM messages
WHERE conversation_id = 1 AND id < 999999999
ORDER BY id DESC
LIMIT 20;计算用户 2 未读数:
sql
SELECT COUNT(*) AS unread_count
FROM messages m
JOIN conversation_members cm ON cm.conversation_id = m.conversation_id
WHERE cm.conversation_id = 1
AND cm.user_id = 2
AND cm.status = 'active'
AND m.sender_id <> 2
AND m.id > COALESCE(cm.last_read_message_id, 0)
AND m.status = 'normal';验证消息幂等唯一:
sql
INSERT INTO messages (conversation_id, sender_id, client_msg_id, message_type, content)
VALUES (1, 1, 'client-001', 'text', JSON_OBJECT('text', 'retry'));预期结果:失败,命中 uk_messages_sender_client_msg。代码中应捕获该冲突并返回已有消息。
代码落地点
models/message_model.go定义Messagecontent在 Go 中建议使用json.RawMessage- 发送消息时必须在同一事务中写入
messages并更新conversations - 历史消息分页以
messages.id作为游标 - 未读数基于
conversation_members.last_read_message_id计算