From 2eb2299709f71aca0fa9b32207db945e750437ac Mon Sep 17 00:00:00 2001 From: pp-bot Date: Tue, 24 Mar 2026 15:45:41 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=A4=9A=E5=9B=BE?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=97=A0=E6=B3=95=E6=98=BE=E7=A4=BA=E7=9A=84?= =?UTF-8?q?=E4=B8=89=E4=B8=AA=E6=A0=B9=E5=9B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因 1 — MessageItem.toEntity() id=0 主键碰撞: WS 拉取的每条消息均用 id=0 insertOrReplace,批量消息相互覆盖, DB 中只留最后一条。改为 id=messageId(服务端唯一 ID)。 根因 2 — SendMessageUseCase 乐观写入 id=0 碰撞: 批量图片发送时所有乐观行共享 id=0,逐条覆盖。 改用负微秒时间戳作为临时唯一 id,HTTP 确认后用真实 messageId 替换。 根因 3 — watchByChatId 无 ORDER BY: DB 消息顺序不确定,宫格分组算法依赖时间升序失败。 在 MessageRepositoryImpl.watchByChatId 及 _buildDisplayItems 中 分别按 sendTime ASC + chatIdx ASC 排序。 Co-Authored-By: Claude Sonnet 4.6 --- .../data/remote/fetch_history_request.dart | 6 ++- .../lib/data/remote/send_message_request.dart | 4 +- .../repositories/message_repository_impl.dart | 12 +++++- .../chat/usecases/send_message_use_case.dart | 39 ++++++++++++++++--- .../features/chat/view/chat_detail_page.dart | 20 +++++++--- 5 files changed, 68 insertions(+), 13 deletions(-) diff --git a/apps/im_app/lib/data/remote/fetch_history_request.dart b/apps/im_app/lib/data/remote/fetch_history_request.dart index 640789d..efb75d9 100644 --- a/apps/im_app/lib/data/remote/fetch_history_request.dart +++ b/apps/im_app/lib/data/remote/fetch_history_request.dart @@ -42,8 +42,12 @@ class MessageItem { atUsers: json['at_users'] as String?, ); + /// [id] 使用服务端 [messageId](必须 > 0)作为主键,确保多条消息在 DB 中唯一共存。 + /// 若 messageId 为 0(防御),退化为负时间戳临时 ID。 Message toEntity() => Message( - id: 0, + id: messageId > 0 + ? messageId + : -(DateTime.now().microsecondsSinceEpoch), messageId: messageId, chatId: chatId, chatIdx: chatIdx, diff --git a/apps/im_app/lib/data/remote/send_message_request.dart b/apps/im_app/lib/data/remote/send_message_request.dart index 46de798..bf25eb0 100644 --- a/apps/im_app/lib/data/remote/send_message_request.dart +++ b/apps/im_app/lib/data/remote/send_message_request.dart @@ -35,7 +35,9 @@ class SendMessageResponse { required int typ, }) => Message( - id: 0, + id: messageId > 0 + ? messageId + : -(DateTime.now().microsecondsSinceEpoch), messageId: messageId, chatId: chatId, chatIdx: chatIdx, diff --git a/apps/im_app/lib/data/repositories/message_repository_impl.dart b/apps/im_app/lib/data/repositories/message_repository_impl.dart index 04cf0ae..3be3dd4 100644 --- a/apps/im_app/lib/data/repositories/message_repository_impl.dart +++ b/apps/im_app/lib/data/repositories/message_repository_impl.dart @@ -59,7 +59,17 @@ class MessageRepositoryImpl implements MessageRepository { .watchWhere( (t) => t.chatId.equals(chatId), ) - .map((rows) => rows.map(_toEntity).toList()); + .map((rows) { + final entities = rows.map(_toEntity).toList(); + // 按 sendTime ASC 排序,sendTime 相同时按 chatIdx ASC + // 确保连续图片消息的宫格分组逻辑正确工作 + entities.sort((a, b) { + final st = (a.sendTime ?? 0).compareTo(b.sendTime ?? 0); + if (st != 0) return st; + return (a.chatIdx ?? 0).compareTo(b.chatIdx ?? 0); + }); + return entities; + }); } @override diff --git a/apps/im_app/lib/features/chat/usecases/send_message_use_case.dart b/apps/im_app/lib/features/chat/usecases/send_message_use_case.dart index 83907c8..1f1f53c 100644 --- a/apps/im_app/lib/features/chat/usecases/send_message_use_case.dart +++ b/apps/im_app/lib/features/chat/usecases/send_message_use_case.dart @@ -9,9 +9,16 @@ import 'package:im_app/domain/repositories/message_repository.dart'; /// 发送文本消息用例 /// /// ## 执行流程 -/// 1. 乐观写入本地 DB(临时消息,id=0)→ UI 立即刷新 +/// 1. 乐观写入本地 DB(负微秒时间戳作为唯一临时 id)→ UI 立即刷新 /// 2. HTTP POST `/app/api/chat/send-message` → 获取服务端 messageId / chatIdx -/// 3. 更新 ChatRepository 的 lastMsg / lastTyp / lastTime +/// 3. 用真实 messageId 替换临时行(delete tempId → insert realId) +/// 4. 更新 ChatRepository 的 lastMsg / lastTyp / lastTime +/// +/// ## 为什么不用 id=0 +/// +/// 原来所有乐观写入均用 `id=0`(主键),批量发送时会相互覆盖, +/// 导致 DB 只剩最后一条消息。改用负微秒时间戳作为临时 id, +/// 每条消息唯一,不会碰撞。服务端确认后立即替换为正式 messageId。 /// /// DB Stream → StreamProvider → UI 自动重建,无需额外通知。 class SendMessageUseCase { @@ -36,10 +43,11 @@ class SendMessageUseCase { }) async { final sendTime = DateTime.now().millisecondsSinceEpoch ~/ 1000; - // 1. 乐观本地写入 + // 1. 乐观本地写入(唯一负 id,防止批量发送时相互覆盖) + final tempId = -(DateTime.now().microsecondsSinceEpoch); await _messageRepo.insertOrReplace( Message( - id: 0, + id: tempId, chatId: chatId, sendId: currentUid, content: content, @@ -63,7 +71,28 @@ class SendMessageUseCase { debugPrint('[SendMessageUseCase] HTTP error: $e'); } - // 3. 更新 Chat 摘要 + // 3. 用真实 messageId 替换临时行 + if (resp != null && resp.messageId > 0) { + try { + await _messageRepo.delete(tempId); + await _messageRepo.insertOrReplace( + Message( + id: resp.messageId, + messageId: resp.messageId, + chatId: chatId, + chatIdx: resp.chatIdx, + sendId: currentUid, + content: content, + typ: typ, + sendTime: resp.sendTime > 0 ? resp.sendTime : sendTime, + ), + ); + } catch (e) { + debugPrint('[SendMessageUseCase] replace temp row error: $e'); + } + } + + // 4. 更新 Chat 摘要 try { final chat = await _chatRepo.getChat(chatId); if (chat != null) { diff --git a/apps/im_app/lib/features/chat/view/chat_detail_page.dart b/apps/im_app/lib/features/chat/view/chat_detail_page.dart index 434f07c..c61d0e5 100644 --- a/apps/im_app/lib/features/chat/view/chat_detail_page.dart +++ b/apps/im_app/lib/features/chat/view/chat_detail_page.dart @@ -199,18 +199,27 @@ class _ChatDisplayItem { /// - 相邻消息 sendTime 差 < 5 秒 /// - 最多 9 条一组 List<_ChatDisplayItem> _buildDisplayItems(List msgs) { + // 防御性排序:watchByChatId 已排序,此处确保即使上游未排序也能正确分组 + final sorted = [...msgs]..sort((a, b) { + final st = (a.sendTime ?? 0).compareTo(b.sendTime ?? 0); + if (st != 0) return st; + return (a.chatIdx ?? 0).compareTo(b.chatIdx ?? 0); + }); + final items = <_ChatDisplayItem>[]; int i = 0; + // 使用 sorted 而非原始 msgs + final src = sorted; - while (i < msgs.length) { - final curr = msgs[i]; + while (i < src.length) { + final curr = src[i]; if ((curr.typ ?? 1) == 2) { final batch = [curr]; int j = i + 1; - while (j < msgs.length && batch.length < 9) { - final next = msgs[j]; - final prev = msgs[j - 1]; + while (j < src.length && batch.length < 9) { + final next = src[j]; + final prev = src[j - 1]; final timeDiff = ((next.sendTime ?? 0) - (prev.sendTime ?? 0)).abs(); if ((next.typ ?? 1) == 2 && @@ -237,6 +246,7 @@ List<_ChatDisplayItem> _buildDisplayItems(List msgs) { return items; } + // ── 显示项渲染 ──────────────────────────────────────────────────────────────── class _DisplayItemWidget extends StatelessWidget {