From c0ed0d83de78c34dedf550a3aa787f59cfac1115 Mon Sep 17 00:00:00 2001 From: "DESKTOP-L8OUL9F\\gfl0034" <1003614916@qq.com> Date: Sun, 18 Oct 2026 19:26:48 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=91=BD=E4=BB=A4=208=20=E6=8E=A5?= =?UTF-8?q?=E5=8F=97=20last=5Fmsg=5Fid=20=E4=BD=9C=E4=B8=BA=E5=B7=B2?= =?UTF-8?q?=E8=AF=BB=E6=B0=B4=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 客户端已读请求常只有 conversation_id 和 last_msg_id。任一消息 id 大于 0 即写入 last_read_msg_id 并清 Redis 未读,不再因此返回 40001。 Co-authored-by: Cursor --- README.md | 2 +- docs/architecture-v3.md | 8 +++++++- internal/transport/tcp/message.go | 13 +++++++++++-- internal/transport/tcp/read_test.go | 23 +++++++++++++++++++++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 459409a..8de7514 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ offset 23 Body bytes UTF-8 JSON | 5 | SEND_ACK | ← 仅 4 的成功应答 | | 6 | MESSAGE | ← 推送 | | 7 | PUSH_ACK | → 空;`SequenceID` = 推送帧的 SequenceID | -| 8 | READ_MESSAGE | → `{"conversation_id","msg_id"}`;成功 `data` 同名字段,`msg_id` 为落库后的 `last_read_msg_id`(只增不减)。清该成员 Redis 未读,并把会话从 unread ZSET 挪到 read ZSET。水位前进时给该用户其他在线设备推 Command 8(不系统推送、不写 mailbox)。缺 `conversation_id` 或 `msg_id` 为 `40001` | +| 8 | READ_MESSAGE | → `conversation_id`,以及 `msg_id` 或 `last_msg_id`(任一大于 0;两者都有时用 `msg_id`)。成功 `data` 为 `conversation_id` 与 `msg_id`,`msg_id` 为落库后的 `last_read_msg_id`(只增不减)。清该成员 Redis 未读,并把会话从 unread ZSET 挪到 read ZSET。水位前进时给该用户其他在线设备推 Command 8(不系统推送、不写 mailbox)。缺 `conversation_id`,或两个消息 id 都小于等于 0,为 `40001` | | 9 | RECALL_MESSAGE | → `{"msg_id"}` | | 10 | SYNC_INBOX | → 会话列表增量 | | 12 | SYNC_MESSAGES | → 某会话消息增量 | diff --git a/docs/architecture-v3.md b/docs/architecture-v3.md index 100314b..e1de7ec 100644 --- a/docs/architecture-v3.md +++ b/docs/architecture-v3.md @@ -653,7 +653,13 @@ READ 更新 last_read_msg_id,清 Redis 未读,入队通知其他设 {"conversation_id": 1001, "msg_id": 2002} ``` -`msg_id` 是成员已读到的消息 id。成功应答仍是命令 8,Body 为 V3 信封,`data.msg_id` 为写入后的 `last_read_msg_id`(只增不减;更小的 id 不回退)。缺 `conversation_id` 或 `msg_id`(≤0)返回 `40001`。消息不存在为 `40401`;消息不属于该会话为 `40001`;不是成员为 `40301`。 +也接受客户端现用的 `last_msg_id`(可同时带 `peer_id`,服务端不使用该字段): + +```json +{"conversation_id": 1001, "peer_id": 9, "last_msg_id": 2002} +``` + +`msg_id` 与 `last_msg_id` 任一大于 0 即是成员已读到的消息 id;两者都大于 0 时用 `msg_id`。成功应答仍是命令 8,Body 为 V3 信封,`data.msg_id` 为写入后的 `last_read_msg_id`(只增不减;更小的 id 不回退)。缺 `conversation_id`,或两个消息 id 都小于等于 0,返回 `40001`。消息不存在为 `40401`;消息不属于该会话为 `40001`;不是成员为 `40301`。 水位前进时入队:`ToUserIDs` 仅为当前用户,`ExcludeDevice` 为当前设备,`NotPush` 与 `NotMailbox` 均为真。其他在线设备收到命令 8。 diff --git a/internal/transport/tcp/message.go b/internal/transport/tcp/message.go index a4f4b52..dbaf25f 100644 --- a/internal/transport/tcp/message.go +++ b/internal/transport/tcp/message.go @@ -18,6 +18,15 @@ type msgIDBody struct { type readBody struct { ConversationID int64 `json:"conversation_id"` MsgID int64 `json:"msg_id"` + LastMsgID int64 `json:"last_msg_id"` +} + +// readCursor accepts msg_id or last_msg_id. Either value above zero is the read cursor. +func (b readBody) readCursor() int64 { + if b.MsgID > 0 { + return b.MsgID + } + return b.LastMsgID } type syncInboxBody struct { @@ -55,11 +64,11 @@ func (s *Server) handleReadMessage(ctx context.Context, conn *session.Conn, fram return nil, errs.ErrAvailable } var in readBody - if err := json.Unmarshal(frame.Body, &in); err != nil || in.ConversationID <= 0 || in.MsgID <= 0 { + if err := json.Unmarshal(frame.Body, &in); err != nil || in.ConversationID <= 0 || in.readCursor() <= 0 { return nil, errs.ErrInvalidArgument } out, err := s.messages.MarkRead(ctx, conn.UserID(), conn.DeviceID(), &msglogic.ReadInput{ - ConversationID: in.ConversationID, MsgID: in.MsgID, + ConversationID: in.ConversationID, MsgID: in.readCursor(), }) if err != nil { return nil, err diff --git a/internal/transport/tcp/read_test.go b/internal/transport/tcp/read_test.go index cde2caf..6f2d184 100644 --- a/internal/transport/tcp/read_test.go +++ b/internal/transport/tcp/read_test.go @@ -37,6 +37,13 @@ func TestReadMessageRegistered(t *testing.T) { if missing != nil || !errors.Is(err, errs.ErrInvalidArgument) { t.Fatalf("missing fields frame=%v err=%v", missing, err) } + onlyLast, err := srv.router.Dispatch(t.Context(), conn, protocol.Frame{ + Version: protocol.CurrentVersion, Command: protocol.CmdReadMessage, + Body: []byte(`{"conversation_id":` + strconv.FormatInt(convID, 10) + `,"peer_id":1,"last_msg_id":0}`), + }) + if onlyLast != nil || !errors.Is(err, errs.ErrInvalidArgument) { + t.Fatalf("zero cursor frame=%v err=%v", onlyLast, err) + } resp, err := srv.router.Dispatch(t.Context(), conn, protocol.Frame{ Version: protocol.CurrentVersion, Command: protocol.CmdReadMessage, SequenceID: 4, @@ -53,6 +60,22 @@ func TestReadMessageRegistered(t *testing.T) { if err := json.Unmarshal(env.Data, &data); err != nil || data.MsgID != msgID || data.ConversationID != convID { t.Fatalf("data %+v %v", data, err) } + + alias, err := srv.router.Dispatch(t.Context(), conn, protocol.Frame{ + Version: protocol.CurrentVersion, Command: protocol.CmdReadMessage, SequenceID: 5, + Body: []byte(`{"conversation_id":` + strconv.FormatInt(convID, 10) + `,"peer_id":1,"last_msg_id":` + strconv.FormatInt(msgID, 10) + `}`), + }) + if err != nil || alias == nil { + t.Fatalf("last_msg_id %+v %v", alias, err) + } + aliasEnv, err := protocol.DecodeEnvelope(alias.Body) + if err != nil || aliasEnv.Code != protocol.CodeOK { + t.Fatalf("last_msg_id envelope %+v %v", aliasEnv, err) + } + var aliasData msglogic.ReadResult + if err := json.Unmarshal(aliasEnv.Data, &aliasData); err != nil || aliasData.MsgID != msgID || aliasData.ConversationID != convID { + t.Fatalf("last_msg_id data %+v %v", aliasData, err) + } } func readFixture(t *testing.T) (*msglogic.Service, int64, int64, int64) {