fix: 修复多图消息无法显示的三个根因
根因 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 <noreply@anthropic.com>
This commit is contained in:
@@ -42,8 +42,12 @@ class MessageItem {
|
|||||||
atUsers: json['at_users'] as String?,
|
atUsers: json['at_users'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// [id] 使用服务端 [messageId](必须 > 0)作为主键,确保多条消息在 DB 中唯一共存。
|
||||||
|
/// 若 messageId 为 0(防御),退化为负时间戳临时 ID。
|
||||||
Message toEntity() => Message(
|
Message toEntity() => Message(
|
||||||
id: 0,
|
id: messageId > 0
|
||||||
|
? messageId
|
||||||
|
: -(DateTime.now().microsecondsSinceEpoch),
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
chatId: chatId,
|
chatId: chatId,
|
||||||
chatIdx: chatIdx,
|
chatIdx: chatIdx,
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ class SendMessageResponse {
|
|||||||
required int typ,
|
required int typ,
|
||||||
}) =>
|
}) =>
|
||||||
Message(
|
Message(
|
||||||
id: 0,
|
id: messageId > 0
|
||||||
|
? messageId
|
||||||
|
: -(DateTime.now().microsecondsSinceEpoch),
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
chatId: chatId,
|
chatId: chatId,
|
||||||
chatIdx: chatIdx,
|
chatIdx: chatIdx,
|
||||||
|
|||||||
@@ -59,7 +59,17 @@ class MessageRepositoryImpl implements MessageRepository {
|
|||||||
.watchWhere<DriftMessage, $MessagesTable>(
|
.watchWhere<DriftMessage, $MessagesTable>(
|
||||||
(t) => t.chatId.equals(chatId),
|
(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
|
@override
|
||||||
|
|||||||
@@ -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
|
/// 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 自动重建,无需额外通知。
|
/// DB Stream → StreamProvider → UI 自动重建,无需额外通知。
|
||||||
class SendMessageUseCase {
|
class SendMessageUseCase {
|
||||||
@@ -36,10 +43,11 @@ class SendMessageUseCase {
|
|||||||
}) async {
|
}) async {
|
||||||
final sendTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
final sendTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||||
|
|
||||||
// 1. 乐观本地写入
|
// 1. 乐观本地写入(唯一负 id,防止批量发送时相互覆盖)
|
||||||
|
final tempId = -(DateTime.now().microsecondsSinceEpoch);
|
||||||
await _messageRepo.insertOrReplace(
|
await _messageRepo.insertOrReplace(
|
||||||
Message(
|
Message(
|
||||||
id: 0,
|
id: tempId,
|
||||||
chatId: chatId,
|
chatId: chatId,
|
||||||
sendId: currentUid,
|
sendId: currentUid,
|
||||||
content: content,
|
content: content,
|
||||||
@@ -63,7 +71,28 @@ class SendMessageUseCase {
|
|||||||
debugPrint('[SendMessageUseCase] HTTP error: $e');
|
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 {
|
try {
|
||||||
final chat = await _chatRepo.getChat(chatId);
|
final chat = await _chatRepo.getChat(chatId);
|
||||||
if (chat != null) {
|
if (chat != null) {
|
||||||
|
|||||||
@@ -199,18 +199,27 @@ class _ChatDisplayItem {
|
|||||||
/// - 相邻消息 sendTime 差 < 5 秒
|
/// - 相邻消息 sendTime 差 < 5 秒
|
||||||
/// - 最多 9 条一组
|
/// - 最多 9 条一组
|
||||||
List<_ChatDisplayItem> _buildDisplayItems(List<Message> msgs) {
|
List<_ChatDisplayItem> _buildDisplayItems(List<Message> 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>[];
|
final items = <_ChatDisplayItem>[];
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
// 使用 sorted 而非原始 msgs
|
||||||
|
final src = sorted;
|
||||||
|
|
||||||
while (i < msgs.length) {
|
while (i < src.length) {
|
||||||
final curr = msgs[i];
|
final curr = src[i];
|
||||||
if ((curr.typ ?? 1) == 2) {
|
if ((curr.typ ?? 1) == 2) {
|
||||||
final batch = <Message>[curr];
|
final batch = <Message>[curr];
|
||||||
int j = i + 1;
|
int j = i + 1;
|
||||||
|
|
||||||
while (j < msgs.length && batch.length < 9) {
|
while (j < src.length && batch.length < 9) {
|
||||||
final next = msgs[j];
|
final next = src[j];
|
||||||
final prev = msgs[j - 1];
|
final prev = src[j - 1];
|
||||||
final timeDiff = ((next.sendTime ?? 0) - (prev.sendTime ?? 0)).abs();
|
final timeDiff = ((next.sendTime ?? 0) - (prev.sendTime ?? 0)).abs();
|
||||||
|
|
||||||
if ((next.typ ?? 1) == 2 &&
|
if ((next.typ ?? 1) == 2 &&
|
||||||
@@ -237,6 +246,7 @@ List<_ChatDisplayItem> _buildDisplayItems(List<Message> msgs) {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── 显示项渲染 ────────────────────────────────────────────────────────────────
|
// ── 显示项渲染 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class _DisplayItemWidget extends StatelessWidget {
|
class _DisplayItemWidget extends StatelessWidget {
|
||||||
|
|||||||
Reference in New Issue
Block a user