package com.example.livestreaming; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.PopupMenu; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.example.livestreaming.databinding.ActivityConversationBinding; import com.example.livestreaming.net.AuthStore; import com.google.android.material.snackbar.Snackbar; import org.json.JSONArray; import org.json.JSONObject; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import okhttp3.Call; import okhttp3.Callback; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; public class ConversationActivity extends AppCompatActivity { private static final String TAG = "ConversationActivity"; private static final String EXTRA_CONVERSATION_ID = "extra_conversation_id"; private static final String EXTRA_CONVERSATION_TITLE = "extra_conversation_title"; private static final String EXTRA_UNREAD_COUNT = "extra_unread_count"; private static final int PAGE_SIZE = 50; // 增加每页消息数量 private static final long POLL_INTERVAL = 3000; // 3秒轮询一次 private ActivityConversationBinding binding; private final OkHttpClient httpClient = new OkHttpClient(); private ConversationMessagesAdapter adapter; private final List messages = new ArrayList<>(); private Handler handler; private Runnable pollRunnable; private boolean isPolling = false; private boolean isUserScrolling = false; // 用户是否正在滚动查看历史记录 private String lastMessageId = null; // 记录最后一条消息ID,用于判断是否有新消息 private boolean isLoadingMore = false; // 是否正在加载更多 private boolean hasMoreHistory = true; // 是否还有更多历史消息 private int historyPage = 1; // 历史消息页码 private String conversationId; private int currentPage = 1; private int initialUnreadCount = 0; private String currentUserId; // 回复消息相关 private ChatMessage replyToMessage = null; public static void start(Context context, String conversationId, String title) { Intent intent = new Intent(context, ConversationActivity.class); intent.putExtra(EXTRA_CONVERSATION_ID, conversationId); intent.putExtra(EXTRA_CONVERSATION_TITLE, title); context.startActivity(intent); } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = ActivityConversationBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); handler = new Handler(Looper.getMainLooper()); String title = getIntent() != null ? getIntent().getStringExtra(EXTRA_CONVERSATION_TITLE) : null; binding.titleText.setText(title != null ? title : "会话"); conversationId = getIntent() != null ? getIntent().getStringExtra(EXTRA_CONVERSATION_ID) : null; initialUnreadCount = getIntent() != null ? getIntent().getIntExtra(EXTRA_UNREAD_COUNT, 0) : 0; // 先尝试从 AuthStore 获取 userId currentUserId = AuthStore.getUserId(this); Log.d(TAG, "从AuthStore获取的用户ID: " + currentUserId); // 如果 userId 为空,尝试从用户信息接口获取 if (currentUserId == null || currentUserId.isEmpty()) { fetchCurrentUserId(); } binding.backButton.setOnClickListener(new DebounceClickListener() { @Override public void onDebouncedClick(View v) { finish(); } }); // 确保在加载消息前获取到正确的userId ensureUserIdLoaded(); setupMessages(); setupInput(); setupReplyInput(); // 标记会话为已读 if (initialUnreadCount > 0 && conversationId != null) { markConversationAsRead(); } } @Override protected void onPause() { super.onPause(); stopPolling(); } @Override protected void onResume() { super.onResume(); startPolling(); } /** * 开始轮询新消息 */ private void startPolling() { if (isPolling) return; isPolling = true; pollRunnable = new Runnable() { @Override public void run() { if (!isPolling || isFinishing() || isDestroyed()) return; loadMessagesFromServer(); handler.postDelayed(this, POLL_INTERVAL); } }; // 延迟开始轮询,避免和初始加载冲突 handler.postDelayed(pollRunnable, POLL_INTERVAL); } /** * 停止轮询 */ private void stopPolling() { isPolling = false; if (pollRunnable != null && handler != null) { handler.removeCallbacks(pollRunnable); } } private void setupMessages() { adapter = new ConversationMessagesAdapter(); adapter.setOnMessageLongClickListener((message, position, view) -> { showMessageMenu(message, position, view); }); LinearLayoutManager layoutManager = new LinearLayoutManager(this); layoutManager.setStackFromEnd(false); binding.messagesRecyclerView.setLayoutManager(layoutManager); binding.messagesRecyclerView.setAdapter(adapter); // 监听滚动状态,判断用户是否在查看历史记录,以及加载更多历史消息 binding.messagesRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { // 用户开始拖动滚动,标记为正在滚动 isUserScrolling = true; Log.d(TAG, "用户开始滚动查看历史记录"); } } @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); // 检查是否滚动到底部 if (!recyclerView.canScrollVertically(1)) { // 已经滚动到底部,重置标志 isUserScrolling = false; Log.d(TAG, "用户已滚动到底部,恢复自动滚动"); } // 检查是否滚动到顶部,加载更多历史消息 if (!recyclerView.canScrollVertically(-1) && dy < 0) { // 滚动到顶部且向上滚动,加载更多历史消息 loadMoreHistory(); } } }); loadMessagesFromServer(); } /** * 确保userId已加载,如果没有则从服务器获取 */ private void ensureUserIdLoaded() { // 先尝试从 AuthStore 获取 currentUserId = AuthStore.getUserId(this); Log.d(TAG, "ensureUserIdLoaded: 从AuthStore获取的userId = " + currentUserId); // 如果为空,尝试从服务器获取 if (currentUserId == null || currentUserId.isEmpty()) { fetchCurrentUserId(); } } /** * 从服务器获取当前用户ID */ private void fetchCurrentUserId() { String token = AuthStore.getToken(this); if (token == null) { Log.w(TAG, "未登录,无法获取用户ID"); return; } String url = ApiConfig.getBaseUrl() + "/api/front/user/info"; Log.d(TAG, "获取用户信息: " + url); Request request = new Request.Builder() .url(url) .addHeader("Authori-zation", token) .get() .build(); httpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e(TAG, "获取用户信息失败", e); } @Override public void onResponse(Call call, Response response) throws IOException { String body = response.body() != null ? response.body().string() : ""; Log.d(TAG, "用户信息响应: " + body); try { JSONObject json = new JSONObject(body); if (json.optInt("code", -1) == 200) { JSONObject data = json.optJSONObject("data"); if (data != null) { int uid = data.optInt("uid", 0); if (uid > 0) { currentUserId = String.valueOf(uid); // 保存到 AuthStore AuthStore.setUserInfo(ConversationActivity.this, currentUserId, data.optString("nickname", "")); Log.d(TAG, "从服务器获取到用户ID: " + currentUserId); // 重新加载消息以正确显示 runOnUiThread(() -> loadMessagesFromServer()); } } } } catch (Exception e) { Log.e(TAG, "解析用户信息失败", e); } } }); } /** * 从服务器加载消息列表 */ private void loadMessagesFromServer() { String token = AuthStore.getToken(this); if (token == null || conversationId == null) { Log.w(TAG, "未登录或会话ID为空"); return; } String url = ApiConfig.getBaseUrl() + "/api/front/conversations/" + conversationId + "/messages?page=" + currentPage + "&pageSize=" + PAGE_SIZE; Log.d(TAG, "加载消息列表: " + url); Request request = new Request.Builder() .url(url) .addHeader("Authori-zation", token) .get() .build(); httpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e(TAG, "加载消息列表失败", e); runOnUiThread(() -> Snackbar.make(binding.getRoot(), "加载消息失败", Snackbar.LENGTH_SHORT).show()); } @Override public void onResponse(Call call, Response response) throws IOException { String body = response.body() != null ? response.body().string() : ""; Log.d(TAG, "消息列表响应: " + body); runOnUiThread(() -> parseMessages(body)); } }); } private void parseMessages(String body) { try { JSONObject json = new JSONObject(body); if (json.optInt("code", -1) == 200) { JSONArray data = json.optJSONArray("data"); // 创建新消息列表 List newMessages = new ArrayList<>(); if (data != null) { for (int i = 0; i < data.length(); i++) { JSONObject item = data.getJSONObject(i); ChatMessage msg = parseChatMessage(item); if (msg != null) { newMessages.add(msg); } } } // 消息按时间倒序返回,需要反转为正序显示 Collections.reverse(newMessages); // 检查是否有新消息(比较最后一条消息ID) boolean hasNewMessages = false; String newLastId = null; if (!newMessages.isEmpty()) { newLastId = newMessages.get(newMessages.size() - 1).getMessageId(); if (lastMessageId == null || !lastMessageId.equals(newLastId)) { hasNewMessages = true; } } // 更新最后消息ID if (newLastId != null) { lastMessageId = newLastId; } // 更新消息列表 messages.clear(); messages.addAll(newMessages); adapter.submitList(new ArrayList<>(messages)); // 只有在用户不滚动查看历史记录时,且有新消息时才滚动到底部 if (!isUserScrolling && hasNewMessages) { Log.d(TAG, "有新消息,自动滚动到底部"); scrollToBottom(); } else if (isUserScrolling) { Log.d(TAG, "用户正在查看历史记录,不自动滚动"); } } else { String msg = json.optString("message", "加载失败"); Snackbar.make(binding.getRoot(), msg, Snackbar.LENGTH_SHORT).show(); } } catch (Exception e) { Log.e(TAG, "解析消息列表失败", e); } } private ChatMessage parseChatMessage(JSONObject item) { try { String messageId = item.optString("messageId", ""); int senderId = item.optInt("userId", 0); // 后端返回的 userId 实际是 senderId String username = item.optString("username", "未知用户"); String message = item.optString("message", ""); long timestamp = item.optLong("timestamp", System.currentTimeMillis()); String status = item.optString("status", "sent"); String avatarUrl = item.optString("avatarUrl", ""); boolean isSystem = item.optBoolean("isSystemMessage", false); String messageType = item.optString("messageType", "text"); // 判断是否是自己发送的消息 // 使用成员变量 currentUserId,确保整个会话期间使用同一个值 String myUserId = currentUserId; if (myUserId == null || myUserId.isEmpty()) { // 如果成员变量为空,尝试从 AuthStore 获取 myUserId = AuthStore.getUserId(this); if (myUserId != null && !myUserId.isEmpty()) { currentUserId = myUserId; // 同步更新成员变量 } } boolean isMine = false; if (myUserId != null && !myUserId.isEmpty() && senderId > 0) { try { // 修复:将 myUserId 转换为整数再比较,避免小数点问题(如 "43.0" vs 43) int myUserIdInt = (int) Double.parseDouble(myUserId); isMine = myUserIdInt == senderId; } catch (NumberFormatException e) { Log.w(TAG, "无法解析userId: " + myUserId); // 降级处理:直接字符串比较 isMine = myUserId.equals(String.valueOf(senderId)); } } Log.d(TAG, "消息判断: myUserId=" + myUserId + ", senderId=" + senderId + ", isMine=" + isMine + ", messageId=" + messageId); // 关键修复:使用 "我" 作为自己发送消息的用户名,确保显示在右侧 String displayName = isMine ? "我" : username; ChatMessage.MessageStatus msgStatus; switch (status) { case "sending": msgStatus = ChatMessage.MessageStatus.SENDING; break; case "read": msgStatus = ChatMessage.MessageStatus.READ; break; default: msgStatus = ChatMessage.MessageStatus.SENT; } ChatMessage chatMessage = new ChatMessage(messageId, displayName, message, timestamp, isSystem, msgStatus); chatMessage.setAvatarUrl(avatarUrl); // 设置消息类型 if ("image".equals(messageType)) { chatMessage.setMessageType(ChatMessage.MessageType.IMAGE); } else if ("voice".equals(messageType)) { chatMessage.setMessageType(ChatMessage.MessageType.VOICE); } else { chatMessage.setMessageType(ChatMessage.MessageType.TEXT); } // 保存原始的 senderId,用于撤回功能判断 chatMessage.setSenderId(senderId); // 解析引用消息信息 // 注意:后端返回的 replyToMessageId 可能是字符串或 null String replyToMessageId = null; if (!item.isNull("replyToMessageId")) { replyToMessageId = item.optString("replyToMessageId", ""); if (replyToMessageId.isEmpty() || "null".equals(replyToMessageId)) { replyToMessageId = null; } } Log.d(TAG, "解析消息 " + messageId + " 的引用信息: replyToMessageId=" + replyToMessageId); if (replyToMessageId != null) { chatMessage.setReplyToMessageId(replyToMessageId); String replyToContent = item.optString("replyToContent", ""); String replyToSenderName = item.optString("replyToSenderName", ""); int replyToSenderId = item.optInt("replyToSenderId", 0); String replyToMessageType = item.optString("replyToMessageType", "text"); chatMessage.setReplyToContent(replyToContent); chatMessage.setReplyToSenderId(replyToSenderId); chatMessage.setReplyToSenderName(replyToSenderName); chatMessage.setReplyToMessageType(replyToMessageType); Log.d(TAG, "解析到引用消息: replyToId=" + replyToMessageId + ", replyToSender=" + replyToSenderName + ", replyToContent=" + replyToContent + ", replyToType=" + replyToMessageType); } return chatMessage; } catch (Exception e) { Log.e(TAG, "解析消息失败", e); return null; } } /** * 标记会话为已读 */ private void markConversationAsRead() { String token = AuthStore.getToken(this); if (token == null || conversationId == null) return; String url = ApiConfig.getBaseUrl() + "/api/front/conversations/" + conversationId + "/read"; Log.d(TAG, "标记已读: " + url); Request request = new Request.Builder() .url(url) .addHeader("Authori-zation", token) .post(RequestBody.create("", MediaType.parse("application/json"))) .build(); httpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e(TAG, "标记已读失败", e); } @Override public void onResponse(Call call, Response response) throws IOException { String body = response.body() != null ? response.body().string() : ""; Log.d(TAG, "标记已读响应: " + body); runOnUiThread(() -> { if (initialUnreadCount > 0) { UnreadMessageManager.decrementUnreadCount(ConversationActivity.this, initialUnreadCount); } }); } }); } private void showMessageMenu(ChatMessage message, int position, View anchorView) { PopupMenu popupMenu = new PopupMenu(this, anchorView); popupMenu.getMenu().add(0, 0, 0, "复制"); // 已撤回的消息不能被回复 if (!"[消息已撤回]".equals(message.getMessage())) { popupMenu.getMenu().add(0, 3, 0, "回复"); } // 只有自己发送的消息才能删除和撤回 if ("我".equals(message.getUsername())) { popupMenu.getMenu().add(0, 1, 0, "删除"); // 检查是否在2分钟内,可以撤回(已撤回的消息不能再撤回) if (!"[消息已撤回]".equals(message.getMessage())) { long now = System.currentTimeMillis(); long messageTime = message.getTimestamp(); long diffMinutes = (now - messageTime) / (1000 * 60); if (diffMinutes <= 2) { popupMenu.getMenu().add(0, 2, 0, "撤回"); } } } popupMenu.setOnMenuItemClickListener(item -> { if (item.getItemId() == 0) { copyMessage(message); return true; } else if (item.getItemId() == 1) { deleteMessage(message, position); return true; } else if (item.getItemId() == 2) { recallMessage(message, position); return true; } else if (item.getItemId() == 3) { showReplyPreview(message); return true; } return false; }); popupMenu.show(); } private void copyMessage(ChatMessage message) { ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText("消息", message.getMessage()); clipboard.setPrimaryClip(clip); Snackbar.make(binding.getRoot(), "已复制到剪贴板", Snackbar.LENGTH_SHORT).show(); } private void deleteMessage(ChatMessage message, int position) { if (position < 0 || position >= messages.size()) return; new AlertDialog.Builder(this) .setTitle("删除消息") .setMessage("确定要删除这条消息吗?") .setPositiveButton("删除", (dialog, which) -> deleteMessageFromServer(message, position)) .setNegativeButton("取消", null) .show(); } private void deleteMessageFromServer(ChatMessage message, int position) { String token = AuthStore.getToken(this); if (token == null) return; String messageId = message.getMessageId(); String url = ApiConfig.getBaseUrl() + "/api/front/conversations/messages/" + messageId; Log.d(TAG, "删除消息: " + url); Request request = new Request.Builder() .url(url) .addHeader("Authori-zation", token) .delete() .build(); httpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e(TAG, "删除消息失败", e); runOnUiThread(() -> Snackbar.make(binding.getRoot(), "删除失败", Snackbar.LENGTH_SHORT).show()); } @Override public void onResponse(Call call, Response response) throws IOException { String body = response.body() != null ? response.body().string() : ""; Log.d(TAG, "删除消息响应: " + body); runOnUiThread(() -> { try { JSONObject json = new JSONObject(body); if (json.optInt("code", -1) == 200) { messages.remove(position); adapter.submitList(new ArrayList<>(messages)); Snackbar.make(binding.getRoot(), "消息已删除", Snackbar.LENGTH_SHORT).show(); } else { Snackbar.make(binding.getRoot(), json.optString("message", "删除失败"), Snackbar.LENGTH_SHORT).show(); } } catch (Exception e) { Log.e(TAG, "解析删除响应失败", e); } }); } }); } /** * 撤回消息 */ private void recallMessage(ChatMessage message, int position) { if (position < 0 || position >= messages.size()) return; new AlertDialog.Builder(this) .setTitle("撤回消息") .setMessage("确定要撤回这条消息吗?") .setPositiveButton("撤回", (dialog, which) -> recallMessageFromServer(message, position)) .setNegativeButton("取消", null) .show(); } private void recallMessageFromServer(ChatMessage message, int position) { String token = AuthStore.getToken(this); if (token == null) return; String messageId = message.getMessageId(); String url = ApiConfig.getBaseUrl() + "/api/front/conversations/messages/" + messageId + "/recall"; Log.d(TAG, "撤回消息: " + url); Request request = new Request.Builder() .url(url) .addHeader("Authori-zation", token) .post(RequestBody.create("", MediaType.parse("application/json"))) .build(); httpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e(TAG, "撤回消息失败", e); runOnUiThread(() -> Snackbar.make(binding.getRoot(), "撤回失败", Snackbar.LENGTH_SHORT).show()); } @Override public void onResponse(Call call, Response response) throws IOException { String body = response.body() != null ? response.body().string() : ""; Log.d(TAG, "撤回消息响应: " + body); runOnUiThread(() -> { try { JSONObject json = new JSONObject(body); if (json.optInt("code", -1) == 200) { // 更新本地消息显示为已撤回 message.setMessage("[消息已撤回]"); adapter.notifyItemChanged(position); Snackbar.make(binding.getRoot(), "消息已撤回", Snackbar.LENGTH_SHORT).show(); } else { String errorMsg = json.optString("message", "撤回失败"); Snackbar.make(binding.getRoot(), errorMsg, Snackbar.LENGTH_SHORT).show(); } } catch (Exception e) { Log.e(TAG, "解析撤回响应失败", e); } }); } }); } private void setupInput() { binding.sendButton.setOnClickListener(new DebounceClickListener(300) { @Override public void onDebouncedClick(View v) { sendMessage(); } }); binding.messageInput.setOnEditorActionListener((v, actionId, event) -> { if (event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { sendMessage(); return true; } return false; }); binding.messageInput.setOnFocusChangeListener((v, hasFocus) -> { if (hasFocus) scrollToBottom(); }); } /** * 设置回复预览区域 */ private void setupReplyInput() { // 关闭回复预览 binding.replyInputClose.setOnClickListener(v -> cancelReply()); } /** * 显示回复预览 */ private void showReplyPreview(ChatMessage message) { replyToMessage = message; Log.d(TAG, "showReplyPreview: 设置回复消息 messageId=" + message.getMessageId() + ", content=" + message.getMessage()); binding.replyInputContainer.setVisibility(View.VISIBLE); // 设置回复标签 String senderName = message.getUsername(); if ("我".equals(senderName)) { senderName = "自己"; } binding.replyInputLabel.setText("回复 " + senderName); // 设置回复内容预览 String content = message.getMessage(); if (message.getMessageType() == ChatMessage.MessageType.IMAGE) { content = "[图片]"; } else if (message.getMessageType() == ChatMessage.MessageType.VOICE) { content = "[语音]"; } binding.replyInputContent.setText(content); // 聚焦输入框 binding.messageInput.requestFocus(); } /** * 取消回复 */ private void cancelReply() { replyToMessage = null; binding.replyInputContainer.setVisibility(View.GONE); } private void sendMessage() { if (!AuthHelper.requireLoginWithToast(this, "发送私信需要登录")) { return; } String text = binding.messageInput.getText() != null ? binding.messageInput.getText().toString().trim() : ""; if (TextUtils.isEmpty(text)) return; if (conversationId == null) { Snackbar.make(binding.getRoot(), "会话ID无效", Snackbar.LENGTH_SHORT).show(); return; } Log.d(TAG, "sendMessage: 开始发送消息, replyToMessage=" + (replyToMessage != null ? replyToMessage.getMessageId() : "null")); // 先在本地显示消息(发送中状态) ChatMessage newMessage = new ChatMessage("我", text); newMessage.setStatus(ChatMessage.MessageStatus.SENDING); // 如果是回复消息,设置引用信息 if (replyToMessage != null) { newMessage.setReplyToMessageId(replyToMessage.getMessageId()); // 如果引用的是自己的消息,显示"自己"而不是"我" String replyToSenderName = replyToMessage.getUsername(); if ("我".equals(replyToSenderName)) { replyToSenderName = "自己"; } newMessage.setReplyToSenderName(replyToSenderName); newMessage.setReplyToContent(replyToMessage.getMessage()); if (replyToMessage.getMessageType() != null) { newMessage.setReplyToMessageType(replyToMessage.getMessageType().name().toLowerCase()); } Log.d(TAG, "设置本地引用信息: replyToId=" + replyToMessage.getMessageId() + ", replyToSender=" + replyToSenderName + ", replyToContent=" + replyToMessage.getMessage()); } else { Log.d(TAG, "sendMessage: 没有引用消息"); } messages.add(newMessage); adapter.submitList(new ArrayList<>(messages)); binding.messageInput.setText(""); scrollToBottom(); // 清除回复状态 ChatMessage replyMsg = replyToMessage; cancelReply(); // 发送到服务器 sendMessageToServer(text, newMessage, replyMsg); } private void sendMessageToServer(String text, ChatMessage localMessage, ChatMessage replyMsg) { String token = AuthStore.getToken(this); if (token == null) return; String url = ApiConfig.getBaseUrl() + "/api/front/conversations/" + conversationId + "/messages"; Log.d(TAG, "发送消息: " + url); try { JSONObject body = new JSONObject(); body.put("message", text); body.put("messageType", "text"); // 如果是回复消息,添加引用消息ID if (replyMsg != null && replyMsg.getMessageId() != null) { try { // 尝试将messageId转换为Long long replyToId = Long.parseLong(replyMsg.getMessageId()); body.put("replyToMessageId", replyToId); Log.d(TAG, "发送回复消息,引用ID: " + replyToId + ", 引用内容: " + replyMsg.getMessage() + ", 引用发送者: " + replyMsg.getUsername()); } catch (NumberFormatException e) { Log.w(TAG, "无法解析回复消息ID: " + replyMsg.getMessageId()); } } Log.d(TAG, "发送消息请求体: " + body.toString()); Request request = new Request.Builder() .url(url) .addHeader("Authori-zation", token) .post(RequestBody.create(body.toString(), MediaType.parse("application/json"))) .build(); httpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e(TAG, "发送消息失败", e); runOnUiThread(() -> { Snackbar.make(binding.getRoot(), "发送失败", Snackbar.LENGTH_SHORT).show(); // 移除发送失败的消息 messages.remove(localMessage); adapter.submitList(new ArrayList<>(messages)); }); } @Override public void onResponse(Call call, Response response) throws IOException { String responseBody = response.body() != null ? response.body().string() : ""; Log.d(TAG, "发送消息响应: " + responseBody); runOnUiThread(() -> handleSendResponse(responseBody, localMessage)); } }); } catch (Exception e) { Log.e(TAG, "构建请求失败", e); } } private void handleSendResponse(String responseBody, ChatMessage localMessage) { try { JSONObject json = new JSONObject(responseBody); if (json.optInt("code", -1) == 200) { JSONObject data = json.optJSONObject("data"); if (data != null) { // 更新本地消息的ID和状态 localMessage.setMessageId(data.optString("messageId", localMessage.getMessageId())); localMessage.setStatus(ChatMessage.MessageStatus.SENT); adapter.notifyItemChanged(messages.indexOf(localMessage)); } } else { String msg = json.optString("message", "发送失败"); Snackbar.make(binding.getRoot(), msg, Snackbar.LENGTH_SHORT).show(); messages.remove(localMessage); adapter.submitList(new ArrayList<>(messages)); } } catch (Exception e) { Log.e(TAG, "解析发送响应失败", e); } } private void scrollToBottom() { if (messages.isEmpty()) return; binding.messagesRecyclerView.post(() -> { int lastPosition = messages.size() - 1; if (lastPosition >= 0) { binding.messagesRecyclerView.smoothScrollToPosition(lastPosition); } }); } /** * 加载更多历史消息 */ private void loadMoreHistory() { if (isLoadingMore || !hasMoreHistory) { return; } String token = AuthStore.getToken(this); if (token == null || conversationId == null) { return; } isLoadingMore = true; historyPage++; String url = ApiConfig.getBaseUrl() + "/api/front/conversations/" + conversationId + "/messages?page=" + historyPage + "&pageSize=" + PAGE_SIZE; Log.d(TAG, "加载更多历史消息: " + url); Request request = new Request.Builder() .url(url) .addHeader("Authori-zation", token) .get() .build(); httpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e(TAG, "加载历史消息失败", e); runOnUiThread(() -> { isLoadingMore = false; historyPage--; }); } @Override public void onResponse(Call call, Response response) throws IOException { String body = response.body() != null ? response.body().string() : ""; Log.d(TAG, "历史消息响应: " + body); runOnUiThread(() -> parseHistoryMessages(body)); } }); } /** * 解析历史消息并添加到列表头部 */ private void parseHistoryMessages(String body) { isLoadingMore = false; try { JSONObject json = new JSONObject(body); if (json.optInt("code", -1) == 200) { JSONArray data = json.optJSONArray("data"); if (data == null || data.length() == 0) { hasMoreHistory = false; Snackbar.make(binding.getRoot(), "没有更多历史消息了", Snackbar.LENGTH_SHORT).show(); return; } // 解析历史消息 List historyMessages = new ArrayList<>(); for (int i = 0; i < data.length(); i++) { JSONObject item = data.getJSONObject(i); ChatMessage msg = parseChatMessage(item); if (msg != null) { historyMessages.add(msg); } } // 消息按时间倒序返回,需要反转为正序显示 Collections.reverse(historyMessages); if (historyMessages.isEmpty()) { hasMoreHistory = false; return; } // 保存当前滚动位置 LinearLayoutManager layoutManager = (LinearLayoutManager) binding.messagesRecyclerView.getLayoutManager(); int firstVisiblePosition = layoutManager != null ? layoutManager.findFirstVisibleItemPosition() : 0; // 将历史消息添加到列表头部 messages.addAll(0, historyMessages); adapter.submitList(new ArrayList<>(messages)); // 恢复滚动位置,保持用户当前查看的消息可见 if (layoutManager != null) { layoutManager.scrollToPosition(firstVisiblePosition + historyMessages.size()); } Log.d(TAG, "加载了 " + historyMessages.size() + " 条历史消息"); } } catch (Exception e) { Log.e(TAG, "解析历史消息失败", e); } } @Override protected void onDestroy() { super.onDestroy(); stopPolling(); handler = null; } }