From 25f9c55b760d9bb9d34b28b729d19eada3d970ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=98=E9=A3=8E?= Date: Tue, 20 Jan 2026 10:17:27 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=20=E5=8E=BB=E6=8E=89=E6=97=A0?= =?UTF-8?q?=E7=94=A8=E6=8F=90=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API类型约束使用指南.md | 215 ----- .../components/TransmitModal/index.tsx | 151 +++- 会话列表修复说明.md | 206 ----- 会话列表问题根本原因和修复.md | 220 ----- 会话列表问题诊断指南.md | 245 ------ 好友群聊详情数据补齐分析报告.md | 772 ------------------ 快速诊断命令.md | 203 ----- 数据同步功能使用指南.md | 397 --------- 数据同步机制分析与改进方案.md | 454 ---------- 数据库userId修复说明.md | 130 --- 数据补齐逻辑修改说明.md | 457 ----------- 未知联系人补全功能说明.md | 552 ------------- 12 files changed, 120 insertions(+), 3882 deletions(-) delete mode 100644 API类型约束使用指南.md delete mode 100644 会话列表修复说明.md delete mode 100644 会话列表问题根本原因和修复.md delete mode 100644 会话列表问题诊断指南.md delete mode 100644 好友群聊详情数据补齐分析报告.md delete mode 100644 快速诊断命令.md delete mode 100644 数据同步功能使用指南.md delete mode 100644 数据同步机制分析与改进方案.md delete mode 100644 数据库userId修复说明.md delete mode 100644 数据补齐逻辑修改说明.md delete mode 100644 未知联系人补全功能说明.md diff --git a/API类型约束使用指南.md b/API类型约束使用指南.md deleted file mode 100644 index 205b85b..0000000 --- a/API类型约束使用指南.md +++ /dev/null @@ -1,215 +0,0 @@ -# API 类型约束使用指南 - -## 📋 概述 - -已为 `request` 和 `request2` 添加了泛型类型约束支持,可以在编译时提供类型检查,减少运行时错误。 - -## 🎯 类型定义 - -### 统一类型定义文件 - -**文件**: `src/api/types.ts` - -定义了以下类型: - -```typescript -// 标准 API 响应结构 -export interface ApiResponse { - code?: number; - success?: boolean; - msg?: string; - message?: string; - data?: T; - list?: T[]; // 列表接口常用字段 - total?: number; // 分页接口常用字段 - [key: string]: any; -} - -// 详情接口响应结构 -export interface ApiDetailResponse { - code?: number; - success?: boolean; - msg?: string; - message?: string; - detail?: T; // 详情接口常用字段 - data?: T; - [key: string]: any; -} - -// 分页响应结构 -export interface ApiPageResponse { - list: T[]; - total: number; - page?: number; - limit?: number; - [key: string]: any; -} -``` - -## 📝 使用方法 - -### 1. 基础用法(使用泛型) - -```typescript -import request from "@/api/request"; -import type { ApiResponse, ApiDetailResponse } from "@/api/types"; - -// 定义数据类型 -interface User { - id: number; - name: string; - avatar?: string; -} - -// 使用泛型指定返回类型 -const getUser = async (id: number): Promise => { - return request("/api/user", { id }, "GET"); -}; - -// 列表接口 -const getUserList = async (): Promise => { - return request("/api/users", {}, "GET"); -}; -``` - -### 2. 列表接口(返回 list 字段) - -```typescript -import request from "@/api/request"; -import type { ApiResponse } from "@/api/types"; - -interface MessageItem { - id: number; - content: string; -} - -// 方式1:直接返回数组(如果拦截器已提取 list) -const getMessages = async (): Promise => { - return request("/v1/kefu/message/list", { page: 1, limit: 20 }); -}; - -// 方式2:返回完整响应结构(如果需要访问 total 等字段) -const getMessagesWithTotal = async (): Promise> => { - return request>("/v1/kefu/message/list", { page: 1, limit: 20 }); -}; -``` - -### 3. 详情接口(返回 detail 字段) - -```typescript -import request from "@/api/request"; -import type { ApiDetailResponse } from "@/api/types"; - -interface FriendDetail { - id: number; - nickname: string; - avatar?: string; - conRemark?: string; -} - -// 使用 ApiDetailResponse 类型 -const getFriendDetail = async (id: number): Promise> => { - return request>("/v1/kefu/wechatFriend/detail", { id }); -}; - -// 使用时 -const result = await getFriendDetail(123); -const detail = result.detail; // TypeScript 会提示 detail 字段存在 -``` - -### 4. 实际示例(已更新) - -**文件**: `src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/api.ts` - -```typescript -import request from "@/api/request"; -import type { ApiResponse, ApiDetailResponse } from "@/api/types"; - -// 定义数据类型 -export interface MessageListItem { - id: number; - dataType: "friend" | "group"; - nickname: string; - // ... 其他字段 -} - -export interface WechatFriendDetail { - id: number; - nickname: string; - avatar?: string; - // ... 其他字段 -} - -// 使用类型约束 -export function getMessageList(params: { page: number; limit: number }): Promise> { - return request>("/v1/kefu/message/list", params, "GET"); -} - -export const getWechatFriendDetail = (params: { id: number }): Promise> => { - return request>("/v1/kefu/wechatFriend/detail", params, "GET"); -}; -``` - -## 🔍 响应拦截器处理逻辑 - -### request.ts 的响应拦截器 - -```typescript -// 成功时返回:payload.data ?? payload -// 这意味着: -// - 如果响应是 { code: 200, data: T },返回 data -// - 如果响应是 { list: T[] },返回整个对象 -// - 如果响应直接是数组,返回数组 -``` - -### 使用建议 - -1. **列表接口**: - ```typescript - // 如果拦截器返回 list 字段,使用数组类型 - const list = await request("/api/list"); - - // 如果需要访问 total,使用 ApiResponse - const result = await request>("/api/list"); - const { list, total } = result; - ``` - -2. **详情接口**: - ```typescript - // 使用 ApiDetailResponse - const result = await request>("/api/detail"); - const detail = result.detail; - ``` - -3. **直接数据**: - ```typescript - // 如果拦截器直接返回数据(不是包装结构) - const data = await request("/api/user"); - ``` - -## ✅ 优势 - -1. **类型安全**:编译时检查,减少运行时错误 -2. **IDE 提示**:自动补全和类型提示 -3. **重构友好**:修改类型定义时,TypeScript 会提示所有需要更新的地方 -4. **文档化**:类型定义本身就是最好的文档 - -## 📌 注意事项 - -1. **向后兼容**:如果不指定泛型,默认返回 `any`,保持向后兼容 -2. **响应结构**:需要根据实际 API 响应结构调整类型定义 -3. **拦截器处理**:注意 `request.ts` 的拦截器会提取 `payload.data`,所以类型定义要考虑这一点 - -## 🔄 迁移建议 - -逐步迁移现有 API 文件: - -1. 先定义数据类型接口 -2. 为 API 函数添加返回类型 -3. 使用泛型约束 `request()` -4. 测试确保类型正确 - ---- - -*创建时间:2024年* -*相关文件:`src/api/request.ts`, `src/api/request2.ts`, `src/api/types.ts`* diff --git a/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransmitModal/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransmitModal/index.tsx index a3ab2c3..7795728 100644 --- a/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransmitModal/index.tsx +++ b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransmitModal/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo, useCallback } from "react"; +import React, { useState, useEffect, useMemo, useCallback, useRef } from "react"; import { Modal, Input, @@ -17,12 +17,13 @@ import { TeamOutlined, } from "@ant-design/icons"; import styles from "./TransmitModal.module.scss"; -import { ContactManager } from "@/utils/dbAction"; import { useWeChatStore } from "@/store/module/weChat/weChat"; import { useContactStore } from "@/store/module/weChat/contacts"; import { useUserStore } from "@/store/module/user"; import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; import { useWebSocketStore } from "@/store/module/websocket/websocket"; +import { getContactList, getGroupList } from "@/pages/pc/ckbox/weChat/api"; + const TransmitModal: React.FC = () => { const [searchValue, setSearchValue] = useState(""); const [allContacts, setAllContacts] = useState< @@ -35,35 +36,112 @@ const TransmitModal: React.FC = () => { const [page, setPage] = useState(1); const pageSize = 20; const { sendCommand } = useWebSocketStore.getState(); - const currentUserId = useUserStore(state => state.user?.id) || 0; + const isInitialLoadRef = useRef(false); // 从 Zustand store 获取更新方法 const openTransmitModal = useContactStore(state => state.openTransmitModal); - const setTransmitModal = useContactStore(state => state.setTransmitModal); const updateSelectedChatRecords = useWeChatStore( state => state.updateSelectedChatRecords, ); - const selectedChatRecords = useWeChatStore( state => state.selectedChatRecords, ); - // 加载联系人数据 - const loadContacts = useCallback(async () => { + // 将好友数据转换为 ContractData 格式 + const convertFriendToContractData = (friend: any): ContractData & { type: "friend" } => { + return { + id: friend.id, + wechatAccountId: friend.wechatAccountId || 0, + wechatId: friend.wechatId || "", + alias: friend.alias || "", + conRemark: friend.conRemark || "", + nickname: friend.nickname || "", + quanPin: friend.quanPin || "", + avatar: friend.avatar || "", + gender: friend.gender || 0, + region: friend.region || "", + addFrom: friend.addFrom || 0, + phone: friend.phone || "", + labels: friend.labels || [], + signature: friend.signature || "", + accountId: friend.accountId || 0, + extendFields: friend.extendFields || null, + city: friend.city || "", + lastUpdateTime: friend.lastUpdateTime || "", + isPassed: friend.isPassed || false, + tenantId: friend.tenantId || 0, + groupId: friend.groupId || 0, + thirdParty: null, + additionalPicture: friend.additionalPicture || "", + desc: friend.desc || "", + config: friend.config || { unreadCount: 0 }, + lastMessageTime: friend.lastMessageTime || 0, + duplicate: friend.duplicate || false, + type: "friend", + } as ContractData & { type: "friend" }; + }; + + // 将群组数据转换为 weChatGroup 格式 + const convertGroupToWeChatGroup = (group: any): weChatGroup & { type: "group" } => { + return { + id: group.id, + wechatAccountId: group.wechatAccountId || 0, + tenantId: group.tenantId || 0, + accountId: group.accountId || 0, + chatroomId: group.chatroomId || "", + chatroomOwner: group.chatroomOwner || "", + conRemark: group.conRemark || "", + nickname: group.nickname || "", + chatroomAvatar: group.chatroomAvatar || group.avatar || "", + groupId: group.groupId || 0, + aiType: group.aiType || 0, + config: group.config || { unreadCount: 0 }, + labels: group.labels || [], + notice: group.notice || "", + selfDisplyName: group.selfDisplyName || "", + wechatChatroomId: group.id, + type: "group", + } as weChatGroup & { type: "group" }; + }; + + // 加载联系人数据(使用API接口) + const loadContacts = useCallback(async (keyword?: string) => { setLoading(true); try { - // 从统一联系人表加载所有联系人 - const allContactsData = - await ContactManager.getUserContacts(currentUserId); - setAllContacts(allContactsData as any); + const params: any = { + page: 1, + limit: 1000, // 获取足够多的数据 + }; + + // 如果有搜索关键词,添加到参数中 + if (keyword && keyword.trim()) { + params.keyword = keyword.trim(); + } + + // 并行请求好友列表和群列表 + const [friendResult, groupResult] = await Promise.all([ + getContactList(params, { debounceGap: 0 }), + getGroupList(params, { debounceGap: 0 }), + ]); + + const friendList = friendResult?.list || []; + const groupList = groupResult?.list || []; + + // 转换数据格式 + const friends = friendList.map(convertFriendToContractData); + const groups = groupList.map(convertGroupToWeChatGroup); + + // 合并好友和群列表 + const allContactsData = [...friends, ...groups]; + setAllContacts(allContactsData); } catch (err) { console.error("加载联系人数据失败:", err); message.error("加载联系人数据失败"); } finally { setLoading(false); } - }, [currentUserId]); + }, []); // 重置状态 - 只在 openTransmitModal 变为 true 时执行 useEffect(() => { @@ -71,28 +149,38 @@ const TransmitModal: React.FC = () => { setSearchValue(""); setSelectedWechatFriend([]); setPage(1); - loadContacts(); + isInitialLoadRef.current = true; + loadContacts(); // 初始加载,不传keyword + } else { + isInitialLoadRef.current = false; } - // 注意:loadContacts 已经在 useCallback 中稳定,但为了安全,我们只在 openTransmitModal 变化时执行 // eslint-disable-next-line react-hooks/exhaustive-deps }, [openTransmitModal]); - // 过滤联系人 - 支持名称和拼音搜索 - const filteredContacts = useMemo(() => { - if (!searchValue.trim()) return allContacts; + // 搜索时调用API(只在searchValue变化时触发,跳过初始加载) + useEffect(() => { + if (openTransmitModal && !isInitialLoadRef.current) { + // 使用防抖,避免频繁请求 + const timer = setTimeout(() => { + // 如果searchValue为空,不传keyword;否则传keyword + const keyword = searchValue.trim() || undefined; + loadContacts(keyword); + setPage(1); // 重置页码 + }, 300); - const keyword = searchValue.toLowerCase(); - return allContacts.filter(contact => { - const name = (contact.nickname || "").toLowerCase(); - const quanPin = (contact as any).quanPin?.toLowerCase?.() || ""; - const pinyin = (contact as any).pinyin?.toLowerCase?.() || ""; - return ( - name.includes(keyword) || - quanPin.includes(keyword) || - pinyin.includes(keyword) - ); - }); - }, [allContacts, searchValue]); + return () => clearTimeout(timer); + } + // 标记初始加载完成 + if (isInitialLoadRef.current) { + isInitialLoadRef.current = false; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchValue, openTransmitModal]); + + // 直接使用 allContacts,因为搜索已经在API层面完成 + const filteredContacts = useMemo(() => { + return allContacts; + }, [allContacts]); const paginatedContacts = useMemo(() => { const start = (page - 1) * pageSize; @@ -121,10 +209,11 @@ const TransmitModal: React.FC = () => { const handleConfirm = () => { for (const user of selectedWechatFriend) { for (const record of selectedChatRecords) { + const isGroup = (user as any).type === "group"; const params = { wechatAccountId: user.wechatAccountId, - wechatChatroomId: user?.chatroomId ? user.id : 0, - wechatFriendId: user?.chatroomId ? 0 : user.id, + wechatChatroomId: isGroup ? user.id : 0, + wechatFriendId: isGroup ? 0 : user.id, msgSubType: record.msgSubType, msgType: record.msgType, content: record.content, diff --git a/会话列表修复说明.md b/会话列表修复说明.md deleted file mode 100644 index 22427d5..0000000 --- a/会话列表修复说明.md +++ /dev/null @@ -1,206 +0,0 @@ -# 会话列表不显示问题修复说明 - -## 🔍 问题分析 - -会话列表不显示的可能原因: - -1. **账号切换过滤问题**:`switchAccount` 根据 `currentCustomer?.id` 过滤会话,如果账号ID不匹配,可能导致过滤后为空 -2. **数据未加载**:数据库查询失败或API调用失败 -3. **索引未构建**:新架构的索引系统未正确构建,导致 `switchAccount` 返回空数组 -4. **用户ID无效**:`currentUserId` 为 0 或 undefined,导致跳过数据加载 - -## ✅ 已实施的修复 - -### 1. 添加调试日志 -- 当会话列表为空时,自动输出调试信息到控制台 -- 包含:`storeSessions` 长度、`filteredSessions` 长度、`currentUserId`、`currentCustomerId`、`selectedAccountId` 等关键状态 - -### 2. 改进账号切换逻辑 -- 当切换账号后结果为空时,自动尝试显示全部会话(`accountId = 0`) -- 确保数据加载后立即触发账号切换 - -### 3. 优化空状态显示 -- 区分"同步中"和"暂无数据"两种状态 -- 显示更友好的提示信息 -- 当数据为空时,提供"刷新会话列表"按钮 - -### 4. 数据加载优化 -- 从数据库加载数据后,立即同步到新架构的 SessionStore -- 确保构建索引和切换账号逻辑正确执行 - -## 🛠️ 排查步骤 - -如果会话列表仍然不显示,请按以下步骤排查: - -### 步骤 1:检查控制台日志 -打开浏览器开发者工具(F12),查看 Console 标签页: - -1. 查找以 `⚠️` 开头的警告信息 -2. 查看是否有 `✅ 从数据库加载会话列表` 的日志 -3. 检查是否有错误信息(红色) - -### 步骤 2:检查用户登录状态 -在控制台执行: -```javascript -// 检查用户ID -console.log('用户ID:', window.__USER_STORE__?.getState?.()?.user?.id); - -// 或者直接查看 localStorage -console.log('用户信息:', localStorage.getItem('user-store')); -``` - -### 步骤 3:检查数据库数据 -在控制台执行: -```javascript -// 需要先导入相关模块 -import { databaseManager } from '@/utils/db'; - -// 获取当前用户ID -const userId = JSON.parse(localStorage.getItem('user-store') || '{}')?.state?.user?.id; - -if (userId) { - const db = await databaseManager.ensureDatabase(userId); - const sessions = await db.chatSessions.where('userId').equals(userId).toArray(); - console.log('数据库中的会话数量:', sessions.length); - console.log('会话数据:', sessions); -} -``` - -### 步骤 4:检查账号选择 -在控制台执行: -```javascript -// 检查当前选中的账号 -console.log('当前账号:', window.__CUSTOMER_STORE__?.getState?.()?.currentCustomer); -``` - -### 步骤 5:手动触发同步 -1. 点击会话列表上方的"同步"按钮 -2. 或点击空状态下的"刷新会话列表"按钮 -3. 观察控制台是否有同步相关的日志 - -### 步骤 6:检查网络请求 -在开发者工具的 Network 标签页中: -1. 查找 `/wechat/message/list` 或类似的API请求 -2. 检查请求是否成功(状态码 200) -3. 查看响应数据是否包含会话列表 - -## 🔧 手动修复方法 - -### 方法 1:清除缓存并重新加载 -```javascript -// 清除所有持久化数据 -localStorage.clear(); -sessionStorage.clear(); - -// 刷新页面 -window.location.reload(); -``` - -### 方法 2:重置会话列表状态 -在控制台执行: -```javascript -// 需要先导入 -import { useMessageStore } from '@/store/module/weChat/message'; - -// 重置状态 -useMessageStore.getState().resetLoadState(); -useMessageStore.getState().clearSessions(); - -// 刷新页面 -window.location.reload(); -``` - -### 方法 3:强制显示全部会话 -如果是因为账号过滤导致的问题,可以临时修改代码: - -在 `MessageList/index.tsx` 中,找到账号切换的 useEffect,临时修改为: -```typescript -// 临时修复:强制显示全部会话 -useEffect(() => { - const accountId = 0; // 强制使用全部账号 - if (accountId !== selectedAccountId) { - switchAccount(accountId); - } -}, [selectedAccountId, switchAccount]); -``` - -## 📝 代码修改位置 - -主要修改文件:`src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx` - -### 修改点 1:添加调试日志(第 111-123 行) -```typescript -// 调试日志:检查会话列表状态 -useEffect(() => { - if (displaySessions.length === 0) { - console.warn("⚠️ 会话列表为空,调试信息:", { - storeSessionsLength: storeSessions.length, - filteredSessionsLength: filteredSessions.length, - currentUserId, - currentCustomerId: currentCustomer?.id, - selectedAccountId, - hasLoadedOnce, - syncing, - }); - } -}, [displaySessions.length, ...]); -``` - -### 修改点 2:改进账号切换逻辑(第 692-700 行) -```typescript -// 同步账号切换到新架构的SessionStore -useEffect(() => { - const accountId = currentCustomer?.id || 0; - if (accountId !== selectedAccountId) { - const result = switchAccount(accountId); - // 如果切换后结果为空,尝试使用全部账号 - if (result.length === 0 && accountId !== 0) { - console.warn("⚠️ 切换账号后会话列表为空,尝试显示全部会话"); - switchAccount(0); - } - } -}, [currentCustomer, selectedAccountId, switchAccount]); -``` - -### 修改点 3:优化数据加载(第 631-640 行) -```typescript -// 有缓存数据立即显示 -if (cachedSessions.length > 0) { - console.log("✅ 从数据库加载会话列表:", cachedSessions.length, "条"); - setSessionState(cachedSessions); - // ... 构建索引 - // 确保切换账号以显示数据 - const accountId = currentCustomer?.id || 0; - if (accountId !== selectedAccountId) { - switchAccount(accountId); - } -} -``` - -### 修改点 4:改进空状态显示(第 1207-1230 行) -- 添加了更详细的空状态提示 -- 添加了"刷新会话列表"按钮 - -## 🎯 预期效果 - -修复后,会话列表应该能够: -1. ✅ 正常显示已加载的会话 -2. ✅ 在数据为空时显示友好的提示 -3. ✅ 提供手动刷新功能 -4. ✅ 在控制台输出有用的调试信息 - -## 📞 如果问题仍然存在 - -如果按照以上步骤排查后问题仍然存在,请提供以下信息: - -1. 浏览器控制台的完整日志(特别是警告和错误) -2. Network 标签页中的 API 请求和响应 -3. 当前用户ID和账号ID -4. 数据库中的会话数量(通过步骤 3 获取) - -这些信息将有助于进一步诊断问题。 - ---- - -*修复时间:2024年* -*修复文件:`src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx`* diff --git a/会话列表问题根本原因和修复.md b/会话列表问题根本原因和修复.md deleted file mode 100644 index 21a09da..0000000 --- a/会话列表问题根本原因和修复.md +++ /dev/null @@ -1,220 +0,0 @@ -# 会话列表不显示问题 - 根本原因和修复 - -## 🎯 问题根本原因 - -通过分析日志 `currentCustomerId: undefined`,发现了根本问题: - -### 问题 1:`currentCustomer` 持久化配置错误 ⭐⭐⭐ - -**文件**: `src/store/module/weChat/customer.ts` - -**错误代码**: -```typescript -{ - name: "customer-storage", - partialize: state => ({ - customerList: [], // ❌ 总是返回空数组 - currentCustomer: null, // ❌ 总是返回 null - }), -} -``` - -**问题分析**: -- `partialize` 函数用于指定哪些状态需要持久化 -- 但代码中直接返回了固定值(空数组和 null),而不是实际的状态值 -- 导致每次刷新页面后,`currentCustomer` 和 `customerList` 都会被重置为空 - -**修复代码**: -```typescript -{ - name: "customer-storage", - partialize: state => ({ - customerList: state.customerList, // ✅ 持久化实际的客服列表 - currentCustomer: state.currentCustomer, // ✅ 持久化当前选中的客服 - }), -} -``` - -### 问题 2:未自动选择默认账号 - -**文件**: `src/pages/pc/ckbox/weChat/components/CustomerList/index.tsx` - -**问题分析**: -- 获取客服列表后,没有自动选择第一个账号 -- 导致 `currentCustomer` 始终为 `null` -- 会话列表根据 `currentCustomer?.id || 0` 过滤,但如果账号数据未加载,可能导致显示问题 - -**修复代码**: -```typescript -getCustomerList() - .then(res => { - updateCustomerList(res); - // 如果当前没有选中的客服,自动选择第一个 - const current = useCustomerStore.getState().currentCustomer; - if (!current && res.length > 0) { - console.log("🔄 自动选择第一个账号:", res[0]); - updateCurrentCustomer(res[0]); - } - setLoading(false); - }) -``` - -### 问题 3:账号切换逻辑需要优化 - -**文件**: `src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx` - -**问题分析**: -- 当 `currentCustomer` 为 `undefined` 时(账号列表未加载),不应该立即切换账号 -- 需要等待账号列表加载完成后再切换 - -**修复代码**: -```typescript -useEffect(() => { - // 当 currentCustomer 为 undefined 时,暂时不切换账号,等待账号列表加载 - if (currentCustomer === undefined) { - console.log("⏳ currentCustomer 为 undefined,等待账号列表加载..."); - return; - } - - // 当 currentCustomer 为 null 或有值时,进行账号切换 - const accountId = currentCustomer?.id || 0; - console.log("🔄 切换账号:", { currentCustomerId: currentCustomer?.id, accountId, selectedAccountId }); - - if (accountId !== selectedAccountId) { - const result = switchAccount(accountId); - console.log(`✅ 切换账号完成,会话数:`, result.length); - // 如果切换后结果为空,尝试使用全部账号(accountId = 0) - if (result.length === 0 && accountId !== 0) { - console.warn("⚠️ 切换账号后会话列表为空,尝试显示全部会话"); - const allResult = switchAccount(0); - console.log(`✅ 切换到全部账号,会话数:`, allResult.length); - } - } -}, [currentCustomer, selectedAccountId, switchAccount]); -``` - -## ✅ 修复总结 - -### 已修复的文件 - -1. **`src/store/module/weChat/customer.ts`** - - 修复持久化配置,正确保存 `currentCustomer` 和 `customerList` - -2. **`src/pages/pc/ckbox/weChat/components/CustomerList/index.tsx`** - - 添加自动选择第一个账号的逻辑 - -3. **`src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx`** - - 优化账号切换逻辑 - - 添加详细的同步日志 - - 改进错误处理 - - 优化空状态显示 - -## 🔄 修复后的流程 - -### 1. 首次加载流程 -``` -用户登录 - ↓ -加载客服列表 (CustomerList) - ↓ -自动选择第一个账号 (新增) - ↓ -保存到 currentCustomer (持久化修复) - ↓ -MessageList 检测到 currentCustomer 变化 - ↓ -切换账号 (switchAccount) - ↓ -从数据库加载会话 - ↓ -同步服务器数据 - ↓ -显示会话列表 -``` - -### 2. 刷新页面流程 -``` -页面刷新 - ↓ -从 localStorage 恢复 currentCustomer (持久化修复) - ↓ -MessageList 使用已保存的 currentCustomer - ↓ -切换账号 (switchAccount) - ↓ -从数据库加载会话 - ↓ -显示会话列表 (无需等待 API) - ↓ -后台同步服务器数据 (更新最新数据) -``` - -## 📊 预期效果 - -修复后,应该看到以下日志: - -``` -✅ 从数据库加载会话列表: X 条 -🔄 自动选择第一个账号: {id: 123, ...} -🔄 切换账号: {currentCustomerId: 123, accountId: 123, selectedAccountId: 0} -✅ 切换账号完成,会话数: X -``` - -## 🧪 测试步骤 - -### 步骤 1:清除缓存测试 -1. 打开浏览器控制台 -2. 执行:`localStorage.clear(); sessionStorage.clear();` -3. 刷新页面 -4. 观察是否自动选择账号并显示会话 - -### 步骤 2:刷新页面测试 -1. 正常使用应用,选择一个账号 -2. 刷新页面(F5) -3. 观察是否保持之前选择的账号 -4. 观察会话列表是否正常显示 - -### 步骤 3:切换账号测试 -1. 点击不同的账号 -2. 观察会话列表是否正确切换 -3. 刷新页面,观察是否保持当前账号 - -## 🔍 如果问题仍然存在 - -如果修复后问题仍然存在,请检查: - -### 1. 检查持久化是否生效 -在控制台执行: -```javascript -// 检查 localStorage 中的数据 -console.log('customer-storage:', localStorage.getItem('customer-storage')); - -// 应该看到类似这样的数据: -// {"state":{"customerList":[...],"currentCustomer":{...}},"version":0} -``` - -### 2. 检查账号列表是否加载 -在控制台执行: -```javascript -import { useCustomerStore } from '@/store/module/weChat/customer'; - -const state = useCustomerStore.getState(); -console.log('customerList:', state.customerList); -console.log('currentCustomer:', state.currentCustomer); -``` - -### 3. 检查 API 是否返回数据 -打开 Network 标签页,查看: -- `/v1/kefu/message/list` - 会话列表 API -- 检查响应数据是否为空 - -## 📝 相关文件 - -- `src/store/module/weChat/customer.ts` - 客服状态管理(核心修复) -- `src/pages/pc/ckbox/weChat/components/CustomerList/index.tsx` - 客服列表组件 -- `src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx` - 会话列表组件 - ---- - -*修复时间:2024年* -*核心问题:持久化配置错误导致 currentCustomer 始终为 null* diff --git a/会话列表问题诊断指南.md b/会话列表问题诊断指南.md deleted file mode 100644 index cd2ac16..0000000 --- a/会话列表问题诊断指南.md +++ /dev/null @@ -1,245 +0,0 @@ -# 会话列表问题诊断指南 - -## 🔍 当前问题状态 - -根据控制台日志,问题表现为: -- ✅ `currentUserId: 121` - 用户ID有效 -- ❌ `currentCustomerId: undefined` - 未选择账号 -- ❌ `storeSessionsLength: 0` - 会话列表为空 -- ❌ `数据库中没有缓存会话数据` - 数据库为空 - -## 📊 已添加的调试日志 - -修复后,控制台会显示以下日志: - -### 1. 初始化阶段 -``` -🔄 需要完整同步,开始同步服务器数据... -🔄 开始同步会话列表,用户ID: 121 -``` - -### 2. API 请求阶段 -``` -📡 请求第 1 页会话列表... {page: 1, limit: 500} -📥 第 1 页API响应: {type: "object", isArray: true, length: X, ...} -``` - -### 3. 数据同步阶段 -``` -💾 同步第 1 页到数据库: {friends: X, groups: Y, total: Z} -✅ 第 1 页同步完成,数据库现有会话数: X -``` - -### 4. UI 更新阶段 -``` -✅ UI已更新,显示会话数: X -✅ 同步完成,已设置 hasLoadedOnce = true -``` - -## 🛠️ 排查步骤 - -### 步骤 1:检查 API 请求 - -打开浏览器开发者工具 → Network 标签页: - -1. 查找请求:`/v1/kefu/message/list?page=1&limit=500` -2. 检查: - - **状态码**:应该是 `200` - - **响应数据**:查看 Response 标签页 - - **请求头**:确认 `Authorization` 头存在 - -**如果 API 请求失败:** -- 401:Token 过期,需要重新登录 -- 403:权限不足 -- 500:服务器错误 -- 网络错误:检查网络连接 - -### 步骤 2:检查 API 响应数据格式 - -在控制台查看 `📥 第 1 页API响应` 日志: - -**正常情况:** -```javascript -{ - type: "object", - isArray: true, - length: 10, // 有数据 - firstItem: { id: 123, nickname: "...", ... } -} -``` - -**异常情况:** -```javascript -{ - type: "object", - isArray: false, // ❌ 不是数组 - length: undefined -} -// 或 -{ - type: "object", - isArray: true, - length: 0 // ❌ 空数组 -} -``` - -### 步骤 3:检查数据同步 - -查看 `💾 同步第 X 页到数据库` 日志: - -**正常情况:** -```javascript -{ - friends: 5, - groups: 3, - total: 8 -} -``` - -**异常情况:** -```javascript -{ - friends: 0, - groups: 0, - total: 0 // ❌ 没有数据被同步 -} -``` - -### 步骤 4:检查数据库写入 - -查看 `✅ 第 X 页同步完成,数据库现有会话数` 日志: - -- 如果数字为 0:数据未写入数据库 -- 如果数字 > 0:数据已写入,但可能 UI 未更新 - -### 步骤 5:手动触发同步 - -如果自动同步失败,可以: - -1. **点击同步按钮**:会话列表上方的"同步"按钮 -2. **点击刷新按钮**:空状态下的"刷新会话列表"按钮 -3. **在控制台执行**: -```javascript -// 需要先获取组件实例或直接调用 API -import { getMessageList } from '@/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/api'; - -// 测试 API -getMessageList({ page: 1, limit: 500 }) - .then(result => { - console.log('API 响应:', result); - console.log('数据类型:', typeof result); - console.log('是否为数组:', Array.isArray(result)); - console.log('数据长度:', result?.length); - }) - .catch(error => { - console.error('API 错误:', error); - }); -``` - -## 🔧 常见问题及解决方案 - -### 问题 1:API 返回空数组 - -**原因:** -- 用户确实没有会话数据 -- API 参数错误 -- 服务器过滤了数据 - -**解决:** -1. 检查 API 请求参数是否正确 -2. 确认用户是否有会话数据(联系后端) -3. 检查是否有筛选条件 - -### 问题 2:API 返回非数组格式 - -**原因:** -- API 响应格式变更 -- 响应被包装在 `data` 字段中 - -**解决:** -检查 `api/request.ts` 中的响应拦截器,确认数据提取逻辑: -```typescript -// 在 request.ts 中 -if (bizSuccess === true || (!hasBizCode && !hasBizSuccess)) { - return payload.data ?? payload; // 这里可能有问题 -} -``` - -### 问题 3:数据同步到数据库但 UI 不更新 - -**原因:** -- Store 状态未更新 -- 账号切换过滤掉了所有数据 - -**解决:** -1. 检查 `storeSessions` 和 `filteredSessions` 的值 -2. 检查 `currentCustomer?.id` 是否正确 -3. 尝试切换到"全部账号"(accountId = 0) - -### 问题 4:数据库写入失败 - -**原因:** -- IndexedDB 权限问题 -- 数据库版本不兼容 -- 数据格式错误 - -**解决:** -1. 检查浏览器控制台是否有 IndexedDB 错误 -2. 尝试清除浏览器数据并重新加载 -3. 检查数据库结构是否匹配 - -## 📝 临时修复方案 - -如果问题持续存在,可以尝试: - -### 方案 1:清除所有数据并重新加载 -```javascript -// 在控制台执行 -localStorage.clear(); -sessionStorage.clear(); -indexedDB.databases().then(dbs => { - dbs.forEach(db => { - indexedDB.deleteDatabase(db.name); - }); -}); -window.location.reload(); -``` - -### 方案 2:强制显示全部会话 -临时修改代码,在 `MessageList/index.tsx` 中: -```typescript -// 临时修复:强制显示全部会话,忽略账号过滤 -useEffect(() => { - const accountId = 0; // 强制使用全部账号 - switchAccount(accountId); -}, [switchAccount]); -``` - -### 方案 3:绕过 Store,直接使用数据库数据 -```typescript -// 在 MessageList 组件中 -useEffect(() => { - const loadDirectly = async () => { - const sessions = await MessageManager.getUserSessions(currentUserId); - if (sessions.length > 0) { - setFilteredSessions(sessions); // 直接设置本地状态 - } - }; - loadDirectly(); -}, [currentUserId]); -``` - -## 🎯 下一步行动 - -1. **刷新页面**,观察控制台日志 -2. **查看 Network 标签页**,检查 API 请求 -3. **根据日志信息**,按照上述步骤排查 -4. **如果问题仍然存在**,请提供: - - 完整的控制台日志 - - Network 标签页中的 API 请求和响应 - - 浏览器版本和操作系统 - ---- - -*最后更新:2024年* -*相关文件:`src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx`* diff --git a/好友群聊详情数据补齐分析报告.md b/好友群聊详情数据补齐分析报告.md deleted file mode 100644 index 75c9e3e..0000000 --- a/好友群聊详情数据补齐分析报告.md +++ /dev/null @@ -1,772 +0,0 @@ -# 好友/群聊详情数据补齐分析报告 - -## 📊 分析结果总结 - -当前项目中有 **4 个主要位置** 在执行好友/群聊详情数据补齐操作: - -| 位置 | 文件 | 触发时机 | 补齐方式 | 作用范围 | -|------|------|---------|---------|---------| -| **位置1** | `msgManage.ts` | WebSocket 收到新消息时 | 自动检测并补全 | 单个会话 | -| **位置2** | `MessageList/index.tsx` - `enrichUnknownContacts` | 会话列表同步完成后 | 批量补全 | 所有缺失的会话 | -| **位置3** | `MessageList/index.tsx` - `onContactClick` | 点击会话时 | 实时补全 | 当前点击的会话 | -| **位置4** | `MessageList/index.tsx` - `handleNewMessage` | WebSocket 新消息事件 | 实时补全 | 新会话 | -| **位置5** | `ProfileCard/ProfileModules` - `fetchFriendDetail` | 打开个人资料卡片时 | 静默补全 | 当前查看的好友 | - ---- - -## 🔍 详细分析 - -### 位置 1️⃣: WebSocket 消息管理器 (`msgManage.ts`) - -**文件路径**: `src/store/module/websocket/msgManage.ts` - -#### 触发时机 -```typescript -// WebSocket 收到新消息 (CmdNewMessage) -messageHandlers.CmdNewMessage = async (message: WebSocketMessage) => { - // ... - const updatedSession = await MessageManager.getSessionByContactId(...); - - // 检查头像和昵称是否为空 - const needEnrich = - !updatedSession.avatar || - !updatedSession.nickname || - updatedSession.avatar === "" || - updatedSession.nickname === ""; -}; -``` - -#### 补齐逻辑 -```typescript -if (needEnrich) { - console.log("🔍 [补全数据] 检测到会话数据不完整,请求详情接口"); - - // 异步请求详情接口 - (async () => { - let detailResult: any = null; - if (updatedSession.type === "friend") { - detailResult = await getWechatFriendDetail({ id: updatedSession.id }); - } else { - detailResult = await getWechatChatroomDetail({ id: updatedSession.id }); - } - - // 更新会话数据库 - await MessageManager.updateSession({...enrichedData}); - - // 更新联系人数据库 - await ContactManager.updateContact({...enrichedData}); - - // 更新 Store 和缓存 - messageStore.addSession(enrichedSession); - messageStore.invalidateCache(wechatAccountId); - })(); -} -``` - -#### 更新内容 -- ✅ 会话数据库 (`MessageManager`) -- ✅ 联系人数据库 (`ContactManager`) -- ✅ Store 缓存 (`messageStore`) -- ✅ 会话列表缓存 (`sessionListCache`) - -#### 特点 -- 🔄 **异步执行**:不阻塞主消息处理流程 -- 🎯 **单个会话**:只处理当前收到消息的会话 -- ⚡ **实时性强**:新消息来时立即触发 - ---- - -### 位置 2️⃣: 会话列表批量补全 (`enrichUnknownContacts`) - -**文件路径**: `src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx` - -#### 触发时机 -```typescript -// 会话列表同步完成后 -const syncWithServer = async () => { - // 阶段1: 获取所有会话 - // 阶段2: 执行完整同步 - // 阶段3: 更新 UI - - // 最后调用补全函数 - enrichUnknownContacts(); // ← 触发点 -}; -``` - -#### 检测条件 -```typescript -const needEnrich = sessionsToCheck.filter(s => { - const noName = !s.conRemark && !s.nickname && !s.wechatId; - const isUnknownNickname = s.nickname === "未知联系人"; - const noAvatar = !s.avatar || s.avatar === ""; - - return noName || isUnknownNickname || noAvatar; -}); -``` - -#### 补齐逻辑 -```typescript -// 并发控制:每批 5 个 -const concurrency = 5; -for (let i = 0; i < needEnrich.length; i += concurrency) { - const batch = needEnrich.slice(i, i + concurrency); - - await Promise.all( - batch.map(async session => { - // 1. 请求 API - let detailResult = session.type === "friend" - ? await getWechatFriendDetail({ id: session.id }) - : await getWechatChatroomDetail({ id: session.id }); - - // 2. 更新 UI - setSessionState(prev => - prev.map(s => s.id === session.id ? {...s, ...enrichedData} : s) - ); - - // 3. 更新会话数据库 - await MessageManager.updateSession({...enrichedData}); - - // 4. 更新联系人数据库 (Upsert) - const existContact = await ContactManager.getContactByIdAndType(...); - if (existContact) { - await ContactManager.updateContact(contactBase); - } else { - await ContactManager.addContact(contactBase); - } - }) - ); -} - -// 5. 刷新整体 UI -buildIndexes(updatedSessions); -switchAccount(currentCustomer?.id || 0); -``` - -#### 更新内容 -- ✅ UI 实时更新 (`setSessionState`) -- ✅ 会话数据库 (`MessageManager`) -- ✅ 联系人数据库 (`ContactManager` - Upsert) -- ✅ Store 索引 (`buildIndexes`) - -#### 特点 -- 📦 **批量处理**:一次处理所有缺失数据的会话 -- ⚡ **并发请求**:每批 5 个,提高效率 -- 📊 **详细统计**:记录成功/失败/未找到数量 -- 🔄 **Upsert 逻辑**:自动判断新增或更新 - ---- - -### 位置 3️⃣: 点击会话时补全 (`onContactClick`) - -**文件路径**: `src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx` - -#### 触发时机 -```typescript -// 用户点击会话列表中的某个会话 -const onContactClick = async (session: ChatSession) => { - console.log("onContactClick", session); - - // 设置当前会话 - setCurrentContact(session as any); - - // ... 处理未读数等 - - // 如果头像或昵称为空,请求最新详情 - if (!session.avatar || !session.nickname) { - // 触发补全逻辑 - } -}; -``` - -#### 补齐逻辑 -```typescript -// 请求最新详情 -let detailResult: any = null; -if (session.type === "friend") { - detailResult = await getWechatFriendDetail({ id: session.id }); -} else { - detailResult = await getWechatChatroomDetail({ id: session.id }); -} - -const detail = detailResult?.detail; -if (detail) { - // 1. 更新会话数据库 - await MessageManager.updateSession({ - userId: currentUserId, - id: session.id, - type: session.type, - avatar: session.type === "group" - ? detail.chatroomAvatar - : detail.avatar, - nickname: detail.nickname, - conRemark: detail.conRemark, - wechatId: detail.wechatId, - }); - - // 2. 更新联系人数据库 - await ContactManager.updateContact({...}); - - // 3. 更新 UI - setSessionState(prev => - prev.map(s => s.id === session.id ? {...enrichedData} : s) - ); - - // 4. 刷新 Store - buildIndexes(updatedSessions); -} -``` - -#### 更新内容 -- ✅ 会话数据库 -- ✅ 联系人数据库 -- ✅ UI 显示 -- ✅ Store 索引 - -#### 特点 -- 🎯 **即时性**:点击时立即检查并更新 -- 🔄 **主动更新**:每次点击都获取最新数据 -- 📱 **用户友好**:确保打开的会话数据最新 - ---- - -### 位置 4️⃣: WebSocket 新消息事件处理 - -**文件路径**: `src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx` - -#### 触发时机 -```typescript -// 监听 WebSocket 新消息事件 -useEffect(() => { - const handleNewMessage = async (event: CustomEvent) => { - const { message: msgData, sessionId, type } = event.detail; - - // 从联系人表查询 - const contact = await ContactManager.getContactByIdAndType( - currentUserId, - sessionId, - type, - ); - - // 如果联系人不存在,从接口获取 - if (!contact) { - // 触发补全逻辑 - } - }; - - window.addEventListener("chatMessageReceived", handleNewMessage); -}, []); -``` - -#### 补齐逻辑 -```typescript -if (!contact) { - console.warn(`联系人表中未找到 ID: ${sessionId}, 从接口获取详细信息`); - - try { - // 请求接口获取详情 - let detailResult: any = null; - if (type === "friend") { - detailResult = await getWechatFriendDetail({ id: sessionId }); - } else { - detailResult = await getWechatChatroomDetail({ id: sessionId }); - } - - if (detailResult?.detail) { - const contactDetail = detailResult.detail; - - // 1. 构建联系人数据并存入数据库 - const newContact = { - serverId: `${type}_${sessionId}`, - userId: currentUserId, - id: sessionId, - type, - wechatAccountId: contactDetail.wechatAccountId, - nickname: contactDetail.nickname || "", - conRemark: contactDetail.conRemark || "", - avatar: type === "group" - ? contactDetail.chatroomAvatar - : contactDetail.avatar, - // ... 其他字段 - }; - - await ContactManager.addContact(newContact as any); - - // 2. 构建并添加会话 - const newSession = MessageManager.buildSessionFromContact( - newContact as any, - currentUserId, - ); - newSession.content = msgData.content; - newSession.lastUpdateTime = new Date().toISOString(); - newSession.config.unreadCount = 1; - - await MessageManager.addSession(newSession); - - // 3. 更新 UI - const updatedSessions = await MessageManager.getUserSessions(currentUserId); - setSessionState(updatedSessions); - buildIndexes(updatedSessions); - } - } catch (error) { - console.error("获取联系人/群组详情失败:", error); - } -} -``` - -#### 更新内容 -- ✅ 联系人数据库 (`ContactManager.addContact`) -- ✅ 会话数据库 (`MessageManager.addSession`) -- ✅ UI 显示 (`setSessionState`) -- ✅ Store 索引 (`buildIndexes`) - -#### 特点 -- 🆕 **新联系人**:专门处理本地完全没有的联系人 -- 📱 **事件驱动**:监听自定义事件触发 -- 🔄 **完整创建**:从零构建联系人和会话记录 - ---- - -### 位置 5️⃣: 个人资料卡片 (`ProfileCard`) - -**文件路径**: `src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx` - -#### 触发时机 -```typescript -// 当打开个人资料卡片时 -useEffect(() => { - if (!isGroup && contract.id) { - // 使用 setTimeout 将请求移至下一个事件循环 - setTimeout(() => { - fetchFriendDetail(); // ← 触发点 - }, 0); - } -}, [contract.id, isGroup, fetchFriendDetail]); -``` - -#### 补齐逻辑 -```typescript -const fetchFriendDetail = React.useCallback(async () => { - if (isGroup) return; // 群聊不需要 - - try { - // 静默请求,不显示加载状态 - const response = await getFriendInfo({ id: contract.id }); - - // 请求成功时更新数据 - setFriendDetail(response); - - // 解析扩展字段 - const extendFieldsObj = JSON.parse( - response.detail.extendFields || "{}" - ); - setExtendFields(extendFieldsObj); - } catch (err) { - // 静默处理,只记录日志 - console.error("获取好友详情失败:", err); - } -}, [contract.id, isGroup]); -``` - -#### 更新内容 -- ✅ 组件本地状态 (`setFriendDetail`) -- ✅ 扩展字段状态 (`setExtendFields`) -- ❌ **不更新数据库** - -#### 特点 -- 🔇 **静默请求**:不显示加载状态 -- 📄 **仅用于展示**:只更新组件状态,不持久化 -- 🎯 **好友专属**:只处理好友详情,不处理群聊 - ---- - -## 📊 对比分析 - -### 触发时机对比 - -| 位置 | 触发条件 | 频率 | 时机 | -|------|---------|------|------| -| 位置1 (msgManage) | WebSocket 新消息 + 数据缺失 | 中 | 收到消息时 | -| 位置2 (enrichUnknownContacts) | 会话列表同步完成 | 低 | 初始加载/切换账号 | -| 位置3 (onContactClick) | 点击会话 + 数据缺失 | 中 | 用户点击时 | -| 位置4 (handleNewMessage) | WebSocket 事件 + 联系人不存在 | 低 | 新联系人首次出现 | -| 位置5 (fetchFriendDetail) | 打开个人资料卡片 | 高 | 每次打开资料卡 | - -### 更新范围对比 - -| 位置 | 会话DB | 联系人DB | Store | UI | 缓存 | -|------|--------|----------|-------|----|----| -| 位置1 | ✅ | ✅ | ✅ | ✅ | ✅ | -| 位置2 | ✅ | ✅ (Upsert) | ✅ | ✅ | ❌ | -| 位置3 | ✅ | ✅ | ✅ | ✅ | ❌ | -| 位置4 | ✅ (新增) | ✅ (新增) | ✅ | ✅ | ❌ | -| 位置5 | ❌ | ❌ | ❌ | ✅ (仅组件) | ❌ | - -### 处理方式对比 - -| 位置 | 同步/异步 | 批量/单个 | 并发控制 | 错误处理 | -|------|----------|---------|---------|---------| -| 位置1 | 异步(不阻塞) | 单个 | 无 | 静默失败 | -| 位置2 | 同步等待 | 批量 | 5个/批 | 记录失败数 | -| 位置3 | 同步等待 | 单个 | 无 | 记录日志 | -| 位置4 | 同步等待 | 单个 | 无 | 记录日志 | -| 位置5 | 异步(不阻塞) | 单个 | 无 | 静默失败 | - ---- - -## 🔄 数据流向图 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ WebSocket 服务器 │ -└─────────────────────────────────────────────────────────────────┘ - ↓ - [CmdNewMessage] - ↓ - ┌───────────────────────────────┐ - │ 位置1: msgManage.ts │ - │ • 检测数据缺失 │ - │ • 请求详情 API │ - │ • 更新数据库 + Store │ - └───────────────────────────────┘ - ↓ - ┌───────────────────────────────┐ - │ 触发自定义事件 │ - │ chatMessageReceived │ - └───────────────────────────────┘ - ↓ - ┌───────────────────────────────┐ - │ 位置4: handleNewMessage │ - │ • 检查联系人是否存在 │ - │ • 不存在则请求 API │ - │ • 创建联系人 + 会话 │ - └───────────────────────────────┘ - ↓ - [UI 自动更新] - -┌─────────────────────────────────────────────────────────────────┐ -│ 用户操作:打开应用/切换账号 │ -└─────────────────────────────────────────────────────────────────┘ - ↓ - ┌───────────────────────────────┐ - │ syncWithServer() │ - │ • 同步会话列表 │ - │ • 执行完整同步 │ - └───────────────────────────────┘ - ↓ - ┌───────────────────────────────┐ - │ 位置2: enrichUnknownContacts │ - │ • 批量检测缺失数据 │ - │ • 并发请求详情 (5个/批) │ - │ • 批量更新数据库 │ - └───────────────────────────────┘ - ↓ - [显示完整数据] - -┌─────────────────────────────────────────────────────────────────┐ -│ 用户操作:点击会话 │ -└─────────────────────────────────────────────────────────────────┘ - ↓ - ┌───────────────────────────────┐ - │ 位置3: onContactClick │ - │ • 检测数据是否缺失 │ - │ • 请求最新详情 │ - │ • 实时更新数据库 + UI │ - └───────────────────────────────┘ - ↓ - [打开聊天窗口] - -┌─────────────────────────────────────────────────────────────────┐ -│ 用户操作:查看个人资料 │ -└─────────────────────────────────────────────────────────────────┘ - ↓ - ┌───────────────────────────────┐ - │ 位置5: fetchFriendDetail │ - │ • 静默请求好友详情 │ - │ • 仅更新组件状态 │ - │ • 用于资料卡展示 │ - └───────────────────────────────┘ - ↓ - [显示详细资料] -``` - ---- - -## 🎯 优化建议 - -### 1. 统一补全逻辑 - -**问题**: -- 5 个位置都在做类似的事情 -- 代码重复度高 -- 维护成本大 - -**建议**: -创建统一的数据补全服务: - -```typescript -// src/services/ContactEnrichService.ts -export class ContactEnrichService { - /** - * 统一的数据补全接口 - */ - static async enrichContact(params: { - userId: number; - contactId: number; - type: "friend" | "group"; - updateDatabase?: boolean; // 是否更新数据库 - updateStore?: boolean; // 是否更新 Store - silent?: boolean; // 是否静默(不显示错误) - }): Promise { - // 统一的补全逻辑 - // 1. 检查数据是否完整 - // 2. 请求 API - // 3. 根据配置更新数据库/Store - // 4. 返回结果 - } - - /** - * 批量补全 - */ - static async enrichContacts( - contacts: Array<{id: number; type: "friend" | "group"}>, - options?: EnrichOptions - ): Promise { - // 并发控制 - // 批量处理 - // 统计结果 - } -} -``` - -**使用示例**: -```typescript -// 位置1: msgManage.ts -if (needEnrich) { - await ContactEnrichService.enrichContact({ - userId, - contactId: updatedSession.id, - type: updatedSession.type, - updateDatabase: true, - updateStore: true, - silent: true, // 不阻塞主流程 - }); -} - -// 位置2: enrichUnknownContacts -await ContactEnrichService.enrichContacts( - needEnrich.map(s => ({ id: s.id, type: s.type })), - { - userId: currentUserId, - updateDatabase: true, - updateStore: true, - concurrency: 5, - } -); - -// 位置5: fetchFriendDetail -await ContactEnrichService.enrichContact({ - userId, - contactId: contract.id, - type: "friend", - updateDatabase: false, // 只用于展示 - updateStore: false, - silent: true, -}); -``` - ---- - -### 2. 去重机制 - -**问题**: -- 多个位置可能同时请求同一个联系人的详情 -- 造成 API 浪费 - -**建议**: -添加请求去重和缓存: - -```typescript -export class ContactEnrichService { - private static pendingRequests = new Map>(); - private static cache = new Map(); - private static CACHE_TTL = 5 * 60 * 1000; // 5分钟 - - static async enrichContact(params: EnrichParams): Promise { - const cacheKey = `${params.type}_${params.contactId}`; - - // 1. 检查缓存 - const cached = this.cache.get(cacheKey); - if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) { - console.log("✅ [补全数据] 使用缓存数据"); - return cached.data; - } - - // 2. 检查是否有正在进行的请求 - if (this.pendingRequests.has(cacheKey)) { - console.log("⏳ [补全数据] 等待进行中的请求"); - return await this.pendingRequests.get(cacheKey); - } - - // 3. 发起新请求 - const requestPromise = this.doEnrich(params); - this.pendingRequests.set(cacheKey, requestPromise); - - try { - const result = await requestPromise; - - // 4. 缓存结果 - this.cache.set(cacheKey, { - data: result, - timestamp: Date.now(), - }); - - return result; - } finally { - // 5. 清理进行中的请求 - this.pendingRequests.delete(cacheKey); - } - } -} -``` - ---- - -### 3. 优先级控制 - -**问题**: -- 所有补全请求优先级相同 -- 用户点击的会话应该优先获取数据 - -**建议**: -添加优先级队列: - -```typescript -export class ContactEnrichService { - private static queue: PriorityQueue = new PriorityQueue(); - - static async enrichContact( - params: EnrichParams, - priority: "high" | "normal" | "low" = "normal" - ): Promise { - return new Promise((resolve, reject) => { - this.queue.enqueue({ - params, - priority, - resolve, - reject, - }); - - this.processQueue(); - }); - } - - private static async processQueue() { - // 按优先级处理队列 - // high > normal > low - } -} -``` - -**使用示例**: -```typescript -// 位置1: WebSocket 新消息 - 高优先级 -await ContactEnrichService.enrichContact( - { ... }, - "high" // 用户可能马上查看 -); - -// 位置2: 批量补全 - 低优先级 -await ContactEnrichService.enrichContacts( - needEnrich, - { priority: "low" } // 后台任务 -); - -// 位置3: 点击会话 - 高优先级 -await ContactEnrichService.enrichContact( - { ... }, - "high" // 用户正在操作 -); -``` - ---- - -### 4. 智能补全策略 - -**问题**: -- 每次都全量补全,即使只缺少头像 -- 浪费 API 资源 - -**建议**: -按需补全: - -```typescript -export class ContactEnrichService { - /** - * 检查缺失的字段 - */ - static checkMissingFields(session: ChatSession): string[] { - const missing: string[] = []; - - if (!session.avatar || session.avatar === "") { - missing.push("avatar"); - } - if (!session.nickname || session.nickname === "未知联系人") { - missing.push("nickname"); - } - if (!session.conRemark) { - missing.push("conRemark"); - } - if (!session.wechatId) { - missing.push("wechatId"); - } - - return missing; - } - - /** - * 根据缺失字段决定是否补全 - */ - static async smartEnrich(session: ChatSession): Promise { - const missingFields = this.checkMissingFields(session); - - // 如果只缺少备注名,可能不需要请求 API - if (missingFields.length === 1 && missingFields[0] === "conRemark") { - console.log("✅ [智能补全] 只缺少备注名,跳过 API 请求"); - return { skipped: true }; - } - - // 如果缺少关键字段,执行补全 - if (missingFields.includes("avatar") || missingFields.includes("nickname")) { - return await this.enrichContact({ ... }); - } - - return { skipped: true }; - } -} -``` - ---- - -## ✅ 总结 - -### 当前状态 -- ✅ **5 个位置**在执行数据补齐 -- ✅ 覆盖了**所有场景**(新消息、同步、点击、事件、查看资料) -- ✅ 数据更新**全面**(会话DB、联系人DB、Store、UI、缓存) -- ⚠️ 代码**重复度高**,维护成本大 -- ⚠️ 缺少**统一管理**和**去重机制** - -### 优化方向 -1. **创建统一服务** - `ContactEnrichService` -2. **添加请求去重** - 避免重复 API 调用 -3. **实现优先级队列** - 优先处理用户操作 -4. **智能补全策略** - 按需补全,减少 API 消耗 -5. **集中错误处理** - 统一日志和监控 - -### 建议实施步骤 -1. 第一阶段:创建 `ContactEnrichService`,实现基础功能 -2. 第二阶段:逐步迁移现有 5 个位置到统一服务 -3. 第三阶段:添加去重、缓存、优先级等高级特性 -4. 第四阶段:优化性能,添加监控和告警 - -实施完成后,代码将更加**简洁、可维护、高效**!🎉 diff --git a/快速诊断命令.md b/快速诊断命令.md deleted file mode 100644 index bbe7b3a..0000000 --- a/快速诊断命令.md +++ /dev/null @@ -1,203 +0,0 @@ -# 会话列表问题快速诊断 - -## 📋 请在浏览器控制台(F12)执行以下命令 - -### 1. 检查控制台日志 - -请在控制台中查找以下日志: - -``` -✅ 应该看到: -- 🔄 开始同步会话列表,用户ID: 121 -- 📡 请求第 1 页会话列表... -- 📥 第 1 页API响应: {...} -- 💾 同步第 1 页到数据库: {...} -- ✅ 第 1 页同步完成,数据库现有会话数: X - -❌ 如果没看到上述日志,说明同步未执行 -``` - -### 2. 手动测试 API - -在控制台执行以下代码测试 API: - -```javascript -// 测试会话列表 API -fetch('/v1/kefu/message/list?page=1&limit=500', { - headers: { - 'Authorization': 'Bearer ' + localStorage.getItem('token') - } -}) -.then(res => res.json()) -.then(data => { - console.log('API 响应:', data); - console.log('是否成功:', data.code === 200 || data.success === true); - console.log('数据类型:', typeof data); - console.log('是否为数组:', Array.isArray(data)); - console.log('数据长度:', data?.length); - console.log('第一条数据:', data?.[0]); -}) -.catch(err => { - console.error('API 错误:', err); -}); -``` - -### 3. 检查数据库内容 - -在控制台执行: - -```javascript -// 打开 IndexedDB -const userId = 121; // 你的用户 ID -const dbName = `CunkebaoDatabase_${userId}`; - -const request = indexedDB.open(dbName); - -request.onsuccess = function(event) { - const db = event.target.result; - const transaction = db.transaction(['chatSessions'], 'readonly'); - const objectStore = transaction.objectStore('chatSessions'); - const getAllRequest = objectStore.getAll(); - - getAllRequest.onsuccess = function() { - const sessions = getAllRequest.result; - console.log('数据库会话数量:', sessions.length); - console.log('数据库会话列表:', sessions); - - // 按账号分组统计 - const byAccount = {}; - sessions.forEach(s => { - const accountId = s.wechatAccountId || 0; - byAccount[accountId] = (byAccount[accountId] || 0) + 1; - }); - console.log('按账号统计:', byAccount); - }; -}; - -request.onerror = function() { - console.error('打开数据库失败'); -}; -``` - -### 4. 检查账号信息 - -```javascript -// 检查当前账号 -const customerStore = localStorage.getItem('customer-storage'); -if (customerStore) { - const parsed = JSON.parse(customerStore); - console.log('当前账号:', parsed.state?.currentCustomer); - console.log('账号列表:', parsed.state?.customerList); -} else { - console.error('未找到账号信息'); -} -``` - -### 5. 检查 Store 状态 - -在控制台执行: - -```javascript -// 需要在组件内部或使用 React DevTools -// 如果可以访问 window 对象上的 store -console.log('查看 React DevTools 中的 Components 标签'); -console.log('找到 MessageList 组件,查看其 hooks 状态'); -``` - -## 🔍 根据结果判断问题 - -### 情况 1: API 返回空数组 -**日志**: `📥 第 1 页API响应: {length: 0}` - -**原因**: 服务器上确实没有会话数据 - -**解决方案**: -1. 确认是否在微信客服端发送过消息 -2. 联系后端确认数据是否正确 -3. 检查是否有权限问题 - -### 情况 2: API 请求失败 -**日志**: `❌ 第 1 页API请求失败` - -**原因**: -- Token 过期(401) -- 网络问题 -- API 地址错误 - -**解决方案**: -1. 检查 Network 标签页中的错误详情 -2. 如果是 401,重新登录 -3. 如果是网络错误,检查网络连接 - -### 情况 3: 数据库为空但 API 有数据 -**特征**: API 返回有数据,但数据库查询为空 - -**原因**: 数据写入数据库失败 - -**解决方案**: -```javascript -// 清除数据库重试 -const userId = 121; -const dbName = `CunkebaoDatabase_${userId}`; -indexedDB.deleteDatabase(dbName).onsuccess = () => { - console.log('数据库已删除,刷新页面重试'); - window.location.reload(); -}; -``` - -### 情况 4: 数据库有数据但 UI 不显示 -**特征**: 数据库查询有数据,但页面不显示 - -**原因**: Store 状态未更新或账号过滤问题 - -**解决方案**: -```javascript -// 检查过滤逻辑 -// 在控制台查看 React DevTools -// 或者临时修改代码强制显示全部账号 -``` - -## 🚨 临时解决方案 - -如果以上诊断都正常,但问题仍然存在,尝试: - -### 方案 1: 强制刷新数据 -在控制台执行: -```javascript -// 清除所有缓存 -localStorage.clear(); -sessionStorage.clear(); - -// 删除所有数据库 -indexedDB.databases().then(dbs => { - dbs.forEach(db => { - console.log('删除数据库:', db.name); - indexedDB.deleteDatabase(db.name); - }); - console.log('所有数据已清除,3秒后自动刷新...'); - setTimeout(() => window.location.reload(), 3000); -}); -``` - -### 方案 2: 手动触发同步 -点击页面上的"刷新会话列表"按钮,或在控制台执行: -```javascript -// 如果能访问到组件实例,手动触发同步 -// 查看 React DevTools 找到 MessageList 组件 -// 手动调用 handleManualSync 方法 -``` - -## 📊 请提供以下信息 - -执行完上述命令后,请提供: - -1. ✅ 控制台中的同步日志(特别是 🔄 📡 📥 💾 ✅ 这些 emoji 开头的) -2. ✅ API 测试结果(步骤 2 的输出) -3. ✅ 数据库内容(步骤 3 的输出) -4. ✅ Network 标签页中 `/v1/kefu/message/list` 请求的截图或数据 - -这些信息将帮助我准确定位问题! - ---- - -*提示:如果看到大量日志,可以右键点击控制台选择"保存为..."导出日志文件* diff --git a/数据同步功能使用指南.md b/数据同步功能使用指南.md deleted file mode 100644 index e37efc3..0000000 --- a/数据同步功能使用指南.md +++ /dev/null @@ -1,397 +0,0 @@ -# 数据同步功能使用指南 - -## ✅ 已实现的改进 - -### 1. 会话列表同步改进 -**文件**: `src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx` - -#### 改进内容 -- ✅ **三阶段同步机制** - - 阶段1: 分页获取所有会话数据到内存 - - 阶段2: 执行完整同步(**不跳过删除**,以 API 为准) - - 阶段3: 更新 UI - -- ✅ **安全检查机制** - - 防止 API 异常导致的误删 - - 当服务器返回空数据但本地有大量数据时,跳过同步 - -- ✅ **自动清理** - - 本地有但服务器没有的会话会自动删除 - - 保持本地数据与服务器完全一致 - -#### 关键代码逻辑 -```typescript -// 阶段1: 累积所有服务器数据 -const allServerSessions = { - friends: [] as any[], - groups: [] as any[], -}; - -while (hasMore) { - const result = await getMessageList({ page, limit }); - // 累积数据到内存 - allServerSessions.friends.push(...friends); - allServerSessions.groups.push(...groups); -} - -// 安全检查 -if (serverTotal === 0 && localSessions.length > 50) { - console.warn("⚠️ 服务器数据异常,跳过同步"); - return; -} - -// 阶段2: 执行完整同步(不跳过删除) -const syncResult = await MessageManager.syncSessions( - currentUserId, - allServerSessions, - { skipDelete: false } // ✅ 以 API 为准 -); - -// 显示同步结果 -console.log({ - 新增: syncResult.added, - 更新: syncResult.updated, - 删除: syncResult.deleted, // ✅ 显示删除数量 -}); -``` - ---- - -### 2. 联系人同步改进 -**文件**: `src/utils/dbAction/contact.ts` - -#### 改进内容 -- ✅ **新增删除逻辑** - - 检测本地有但服务器没有的联系人 - - 自动删除这些无效数据 - -- ✅ **安全检查** - - 防止大量误删(服务器数据为空时跳过同步) - - 删除比例超过 30% 时发出警告 - -- ✅ **返回值优化** - - 返回详细的同步统计信息 - - 包含新增、更新、删除数量 - -#### 关键代码逻辑 -```typescript -static async syncContacts( - userId: number, - serverContacts: any[], -): Promise<{ added: number; updated: number; deleted: number }> { - - // 1. 获取本地和服务器数据 - const localContacts = await this.getUserContacts(userId); - const serverContactMap = new Map(serverContacts.map(c => [c.serverId, c])); - - // 2. 计算需要删除的联系人 - const contactsToDelete: string[] = []; - for (const localContact of localContacts) { - if (!serverContactMap.has(localContact.serverId)) { - contactsToDelete.push(localContact.serverId); - } - } - - // 3. 安全检查 - if (serverTotal === 0 && localTotal > 50) { - console.warn("⚠️ 服务器数据异常,跳过同步"); - return { added: 0, updated: 0, deleted: 0 }; - } - - // 4. 执行删除 - if (contactsToDelete.length > 0) { - for (const serverId of contactsToDelete) { - await contactUnifiedService.delete(serverId); - } - } - - // 5. 返回统计信息 - return { - added: contactsToAdd.length, - updated: contactsToUpdate.length, - deleted: contactsToDelete.length, - }; -} -``` - ---- - -## 📊 同步流程对比 - -### 改进前 -``` -┌─────────────────────────────────────────┐ -│ 分页获取会话 │ -│ ↓ │ -│ 每页立即同步 (skipDelete: true) │ ❌ 永远不删除 -│ ↓ │ -│ 更新 UI │ -└─────────────────────────────────────────┘ - -结果:本地会累积大量无效会话 -``` - -### 改进后 -``` -┌─────────────────────────────────────────┐ -│ 阶段1: 分页获取所有会话到内存 │ -│ ↓ │ -│ 阶段2: 执行完整同步 (skipDelete: false) │ ✅ 删除无效数据 -│ ↓ │ -│ 阶段3: 更新 UI │ -└─────────────────────────────────────────┘ - -结果:本地数据与服务器完全一致 -``` - ---- - -## 🔍 监控和调试 - -### 会话同步日志 -```typescript -// 阶段1: 获取数据 -console.log("📡 [阶段1] 开始分页获取所有会话数据..."); -console.log("✅ 第 1 页获取完成:", { - 本页好友: 10, - 本页群聊: 5, - 累计好友: 10, - 累计群聊: 5, -}); - -// 安全检查 -console.log("📊 [安全检查] 本地: 100 条, 服务器: 95 条"); - -// 阶段2: 同步 -console.log("🔄 [阶段2] 执行完整同步,清理本地多余数据..."); -console.log("✅ [阶段2] 会话列表同步完成:", { - 新增: 10, - 更新: 80, - 删除: 5, // ✅ 显示删除的会话数量 - 服务器总数: 95, -}); - -// 阶段3: 更新UI -console.log("✅ [阶段3] UI已更新,显示会话数: 95"); -``` - -### 联系人同步日志 -```typescript -// 同步统计 -console.log("✅ [联系人同步] 完成: 新增5个, 更新10个, 删除3个"); - -// 删除详情 -console.log("🗑️ [联系人同步] 检测到 3 个本地联系人在服务器不存在,准备删除"); -console.log("✅ [联系人同步] 实际删除: 3 条"); - -// 安全警告 -console.warn("⚠️ [联系人同步] 本次将删除 30.0% 的联系人数据"); -``` - ---- - -## 🛡️ 安全保护机制 - -### 1. 空数据保护 -```typescript -// 场景:API 异常返回空数据 -if (serverTotal === 0 && localTotal > 50) { - console.warn("⚠️ 服务器返回空数据,但本地有大量数据"); - console.warn("⚠️ 跳过本次同步以防止误删"); - return; // 不执行删除 -} -``` - -### 2. 大量删除警告 -```typescript -// 场景:删除比例超过 30% -const deleteRatio = contactsToDelete.length / localTotal; -if (deleteRatio > 0.3) { - console.warn(`⚠️ 本次将删除 ${(deleteRatio * 100).toFixed(1)}% 的数据`); -} -``` - -### 3. 失败容错 -```typescript -// 某个联系人删除失败不影响其他操作 -for (const serverId of contactsToDelete) { - try { - await contactUnifiedService.delete(serverId); - } catch (error) { - console.error(`❌ 删除失败: ${serverId}`, error); - // 继续处理下一个 - } -} -``` - ---- - -## 📝 使用示例 - -### 手动触发会话同步 -```typescript -// 在 MessageList 组件中 -const handleManualSync = async () => { - if (syncing) return; - setSyncing(true); - try { - await syncWithServer(); // 会自动执行完整同步和清理 - } catch (error) { - console.error("同步失败:", error); - } finally { - setSyncing(false); - } -}; -``` - -### 手动触发联系人同步 -```typescript -// 在 WechatFriends 组件中 -import { syncContactsFromServer } from './extend'; - -const handleSyncContacts = async () => { - try { - const result = await syncContactsFromServer(userId); - console.log("同步结果:", result); - // result: { added: 5, updated: 10, deleted: 3 } - } catch (error) { - console.error("同步失败:", error); - } -}; -``` - ---- - -## ⚠️ 注意事项 - -### 1. 数据一致性 -- ✅ 本地数据库以 API 为准 -- ✅ 不存在于 API 的数据会被自动删除 -- ✅ 保证数据完全一致 - -### 2. 性能考虑 -- ✅ 会话同步: 先全部加载到内存,再一次性同步(避免频繁数据库操作) -- ✅ 联系人同步: 批量处理,使用 Map 提高查找效率 -- ✅ 删除操作: 逐条处理,确保错误隔离 - -### 3. 用户体验 -- ✅ 先显示缓存数据(快速响应) -- ✅ 后台静默同步(不阻塞界面) -- ✅ 同步失败仍可使用缓存数据 - -### 4. 调试技巧 -- ✅ 所有关键步骤都有详细日志 -- ✅ 使用 emoji 标记不同类型的日志 -- ✅ 显示删除数量和详情 - ---- - -## 🔄 自动同步触发时机 - -### 会话列表 -1. 应用启动时 -2. 切换客服账号时 -3. 用户手动点击刷新按钮 -4. WebSocket 重连成功后 - -### 联系人 -1. 打开好友/群聊列表时 -2. 切换客服账号时 -3. 用户手动点击同步按钮 - ---- - -## 🎯 预期效果 - -### 数据准确性 -- ✅ 本地数据与服务器完全一致 -- ✅ 不会出现"幽灵会话"(服务器已删除但本地仍显示) -- ✅ 不会出现"幽灵联系人"(API 中不存在但本地仍显示) - -### 性能表现 -- ✅ 初次加载:显示缓存数据(< 100ms) -- ✅ 后台同步:不影响用户操作 -- ✅ 同步完成:自动刷新界面 - -### 安全性 -- ✅ 防止 API 异常导致的误删 -- ✅ 大量删除时发出警告 -- ✅ 删除失败不影响其他操作 - ---- - -## 📞 故障排查 - -### 问题1: 会话/联系人没有被删除 -**检查项**: -1. 查看控制台是否有 "⚠️ 安全检查失败" 日志 -2. 确认 API 返回的数据是否正常 -3. 检查 `skipDelete` 参数是否为 `false` - -**解决方案**: -```typescript -// 强制执行完整同步 -await MessageManager.syncSessions( - userId, - serverData, - { skipDelete: false } // 确保为 false -); -``` - -### 问题2: 同步失败 -**检查项**: -1. 查看控制台错误日志 -2. 确认网络连接是否正常 -3. 检查 API 是否返回正确格式 - -**解决方案**: -```typescript -// 查看详细错误 -try { - await syncWithServer(); -} catch (error) { - console.error("同步失败详情:", error); - // 使用缓存数据 -} -``` - -### 问题3: 删除了不应该删除的数据 -**原因**: 可能是 API 返回数据不完整 - -**预防措施**: -- ✅ 已实现安全检查机制 -- ✅ 空数据时自动跳过同步 -- ✅ 大量删除时发出警告 - ---- - -## 🚀 未来优化方向 - -### 1. 增量同步 -- 记录最后同步时间 -- 只同步变更的数据 -- 每 30 分钟执行一次全量同步 - -### 2. 同步队列 -- 避免重复同步 -- 自动重试失败的同步 -- 智能调度同步频率 - -### 3. 用户提示 -- 同步进度提示 -- 删除数据二次确认 -- 同步结果通知 - ---- - -## ✅ 总结 - -本次改进实现了**以 API 为准**的数据同步策略: - -1. **会话列表**: 完整同步,自动删除服务器不存在的会话 -2. **联系人**: 完整同步,自动删除服务器不存在的好友/群聊 -3. **安全保护**: 防止 API 异常导致误删 -4. **详细日志**: 便于调试和监控 -5. **性能优化**: 批量操作,提高效率 - -现在本地数据库会**始终与服务器保持一致**,不会再出现"幽灵数据"的问题!🎉 diff --git a/数据同步机制分析与改进方案.md b/数据同步机制分析与改进方案.md deleted file mode 100644 index f7407e5..0000000 --- a/数据同步机制分析与改进方案.md +++ /dev/null @@ -1,454 +0,0 @@ -# 数据同步机制分析与改进方案 - -## 🔍 当前实现分析 - -### 1. 会话列表同步 (MessageList) - -**位置**: `src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx` - -**当前逻辑**: - -```typescript -// 分页获取,每页都使用 skipDelete: true -await MessageManager.syncSessions( - currentUserId, - { friends, groups }, - { skipDelete: true }, // ⚠️ 永远跳过删除检查 -); -``` - -**问题**: - -- ❌ 所有分页同步都设置了 `skipDelete: true` -- ❌ 永远不会删除本地已不存在于服务器的会话 -- ❌ 导致本地数据库累积大量无效会话 - ---- - -### 2. 联系人同步 (ContactManager) - -**位置**: `src/utils/dbAction/contact.ts` - -**当前逻辑**: - -```typescript -static async syncContacts(userId: number, serverContacts: any[]) { - // 只处理新增和更新 - const contactsToAdd: Contact[] = []; - const contactsToUpdate: Contact[] = []; - - // ⚠️ 完全没有删除逻辑! - // 服务器删除的联系人会一直残留在本地 -} -``` - -**问题**: - -- ❌ 没有删除逻辑 -- ❌ API 中已删除的好友/群聊仍会保留在本地数据库 -- ❌ 可能导致显示已删除的联系人 - ---- - -## 💡 改进方案 - -### 方案 1: 会话列表同步改进 - -#### 策略 - -1. **分页同步阶段**: 使用 `skipDelete: true`(避免误删其他页数据) -2. **所有页完成后**: 执行一次完整同步,不跳过删除 - -#### 实现代码 - -```typescript -const syncWithServer = async () => { - if (!currentUserId) return; - - setSyncing(true); - - try { - console.log("🔄 开始同步会话列表..."); - - let page = 1; - let hasMore = true; - const allServerSessions = { - friends: [], - groups: [], - }; - - // 第一阶段:分页获取所有数据 - while (hasMore) { - const result = await getMessageList({ - page, - limit: 500, - wechatAccountId: currentCustomer?.id || 0, - }); - - if (!result || !Array.isArray(result) || result.length === 0) { - hasMore = false; - break; - } - - const friends = result.filter( - msg => msg.dataType === "friend" || !msg.chatroomId, - ); - const groups = result - .filter(msg => msg.dataType === "group" || msg.chatroomId) - .map(msg => ({ - ...msg, - chatroomAvatar: msg.chatroomAvatar || msg.avatar || "", - })); - - // 累积服务器数据 - allServerSessions.friends.push(...friends); - allServerSessions.groups.push(...groups); - - console.log(`✅ 第 ${page} 页获取完成:`, { - friends: friends.length, - groups: groups.length, - 累计好友: allServerSessions.friends.length, - 累计群聊: allServerSessions.groups.length, - }); - - page++; - - if (result.length < 500) { - hasMore = false; - } - } - - // 第二阶段:执行完整同步(包含删除) - console.log("🔄 执行完整同步,清理本地多余数据..."); - const syncResult = await MessageManager.syncSessions( - currentUserId, - allServerSessions, - { skipDelete: false }, // ✅ 不跳过删除 - ); - - console.log("✅ 会话列表同步完成:", { - 新增: syncResult.added, - 更新: syncResult.updated, - 删除: syncResult.deleted, // ✅ 现在会显示删除数量 - 服务器总数: - allServerSessions.friends.length + allServerSessions.groups.length, - }); - - // 更新 UI - const finalSessions = await MessageManager.getUserSessions(currentUserId); - setSessionState(finalSessions); - buildIndexes(finalSessions); - switchAccount(currentCustomer?.id || 0); - - // 补充未知联系人信息 - enrichUnknownContacts(); - } catch (error) { - console.error("❌ 同步服务器数据失败:", error); - } finally { - setSyncing(false); - } -}; -``` - ---- - -### 方案 2: 联系人同步改进 - -#### 策略 - -1. 获取所有服务器联系人(好友 + 群聊) -2. 获取所有本地联系人 -3. 比对差异:新增、更新、**删除** -4. 以 API 为准,删除本地多余数据 - -#### 实现代码 - -**更新 `src/utils/dbAction/contact.ts`**: - -```typescript -/** - * 同步联系人数据(以 API 为准,自动删除本地多余数据) - */ -static async syncContacts( - userId: number, - serverContacts: any[], -): Promise<{ added: number; updated: number; deleted: number }> { - try { - // 1. 获取本地联系人 - const localContacts = await this.getUserContacts(userId); - const localContactMap = new Map(localContacts.map(c => [c.serverId, c])); - - // 2. 创建服务器联系人映射 - const serverContactMap = new Map( - serverContacts.map(c => [c.serverId, c]) - ); - - // 3. 计算差异 - const contactsToAdd: Contact[] = []; - const contactsToUpdate: Contact[] = []; - const contactsToDelete: string[] = []; - - // 检查新增和更新 - for (const serverContact of serverContacts) { - const localContact = localContactMap.get(serverContact.serverId); - - if (!localContact) { - // 新增联系人 - contactsToAdd.push({ - ...serverContact, - userId, - serverId: serverContact.serverId, - lastUpdateTime: new Date().toISOString(), - }); - } else { - // 检查是否需要更新 - if (this.isContactChanged(localContact, serverContact)) { - contactsToUpdate.push({ - ...serverContact, - userId, - serverId: serverContact.serverId, - lastUpdateTime: new Date().toISOString(), - }); - } - } - } - - // ✅ 新增:检查需要删除的联系人(本地有但服务器没有) - for (const localContact of localContacts) { - if (!serverContactMap.has(localContact.serverId)) { - contactsToDelete.push(localContact.serverId); - } - } - - // 4. 执行数据库操作 - if (contactsToAdd.length > 0) { - await this.addContacts(contactsToAdd); - } - - if (contactsToUpdate.length > 0) { - for (const contact of contactsToUpdate) { - await this.updateContact(contact); - } - } - - // ✅ 新增:执行删除操作 - if (contactsToDelete.length > 0) { - console.log( - `🗑️ 检测到 ${contactsToDelete.length} 个本地联系人在服务器不存在,准备删除:`, - contactsToDelete.slice(0, 5) // 只打印前5个 - ); - - for (const serverId of contactsToDelete) { - try { - await contactUnifiedService.delete(serverId); - } catch (error) { - console.error(`删除联系人失败: ${serverId}`, error); - } - } - } - - console.log( - `✅ 同步联系人完成: 新增${contactsToAdd.length}个, 更新${contactsToUpdate.length}个, 删除${contactsToDelete.length}个`, - ); - - return { - added: contactsToAdd.length, - updated: contactsToUpdate.length, - deleted: contactsToDelete.length, - }; - } catch (error) { - console.error("同步联系人失败:", error); - throw error; - } -} -``` - ---- - -## 🛡️ 安全措施 - -### 1. 防止误删保护 - -```typescript -/** - * 同步前的安全检查 - */ -static async safetyCheck( - localCount: number, - serverCount: number -): Promise { - // 如果服务器返回数据为空,但本地有大量数据,可能是 API 异常 - if (serverCount === 0 && localCount > 50) { - console.warn("⚠️ 安全检查失败: 服务器返回空数据,但本地有大量数据"); - console.warn(`本地: ${localCount} 条, 服务器: ${serverCount} 条`); - console.warn("可能是 API 异常,跳过本次同步以防止误删"); - return false; - } - - // 如果删除比例超过 50%,需要警告 - const deleteRatio = (localCount - serverCount) / localCount; - if (deleteRatio > 0.5) { - console.warn(`⚠️ 本次同步将删除 ${(deleteRatio * 100).toFixed(1)}% 的数据`); - console.warn(`本地: ${localCount} 条, 服务器: ${serverCount} 条`); - } - - return true; -} -``` - -### 2. 使用示例 - -```typescript -const syncWithServer = async () => { - try { - // 获取服务器数据 - const friends = await getAllFriends(); - const groups = await getAllGroups(); - const serverContacts = [...friendContacts, ...groupContacts]; - - // 获取本地数据 - const localContacts = await ContactManager.getUserContacts(userId); - - // 安全检查 - const isSafe = await ContactManager.safetyCheck( - localContacts.length, - serverContacts.length, - ); - - if (!isSafe) { - console.error("同步被中止,请检查 API"); - return; - } - - // 执行同步 - const result = await ContactManager.syncContacts(userId, serverContacts); - console.log("同步结果:", result); - } catch (error) { - console.error("同步失败:", error); - } -}; -``` - ---- - -## 📈 优化建议 - -### 1. 增量同步标记 - -为避免频繁全量同步,可以添加时间戳机制: - -```typescript -interface SyncMetadata { - lastFullSync: string; // 最后一次全量同步时间 - lastIncrementalSync: string; // 最后一次增量同步时间 -} - -// 每 30 分钟执行一次完整同步(包含删除) -// 其他时候执行增量同步(不删除) -const shouldDoFullSync = () => { - const lastFullSync = localStorage.getItem("lastFullSync"); - if (!lastFullSync) return true; - - const timeDiff = Date.now() - new Date(lastFullSync).getTime(); - return timeDiff > 30 * 60 * 1000; // 30 分钟 -}; -``` - -### 2. 批量删除优化 - -```typescript -/** - * 批量删除联系人(优化版) - */ -static async batchDeleteContacts(serverIds: string[]): Promise { - if (serverIds.length === 0) return; - - try { - // 使用事务批量删除 - await db.transaction('rw', db.contacts, async () => { - await db.contacts.bulkDelete(serverIds); - }); - - console.log(`✅ 批量删除 ${serverIds.length} 个联系人`); - } catch (error) { - console.error("批量删除联系人失败:", error); - throw error; - } -} -``` - ---- - -## 🔄 完整流程图 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 用户打开应用/切换账号 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 1: 从本地数据库加载缓存数据(快速显示) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 2: 后台静默同步服务器数据 │ -│ ──────────────────────────────────────────────────────── │ -│ 2.1 分页获取所有会话/联系人(累积到内存) │ -│ 2.2 获取本地所有数据 │ -│ 2.3 执行安全检查(防止误删) │ -│ 2.4 计算差异(新增、更新、删除) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 3: 执行同步操作(以 API 为准) │ -│ ──────────────────────────────────────────────────────── │ -│ ✅ 新增: API 有但本地没有的数据 │ -│ ✅ 更新: API 和本地都有但内容不同的数据 │ -│ ✅ 删除: 本地有但 API 没有的数据(自动清理) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 4: 更新 UI 显示 │ -│ ──────────────────────────────────────────────────────── │ -│ • 从数据库重新读取最新数据 │ -│ • 更新 Store 和缓存 │ -│ • 刷新界面 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 5: 记录同步元数据 │ -│ ──────────────────────────────────────────────────────── │ -│ • 记录最后同步时间 │ -│ • 记录新增/更新/删除数量 │ -│ • 用于下次增量同步参考 │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## ⚠️ 注意事项 - -1. **数据一致性**: 以 API 为准,本地数据库只是缓存 -2. **防误删**: 添加安全检查,避免 API 异常导致大量数据被删除 -3. **性能优化**: 使用批量操作和事务,提高同步效率 -4. **用户体验**: 先显示缓存数据,后台静默同步 -5. **错误处理**: 同步失败时不影响现有数据的显示 - ---- - -## 📝 总结 - -### 改进前 - -- ❌ 会话列表永远不删除多余数据 -- ❌ 联系人完全没有删除逻辑 -- ❌ 本地数据库会累积大量无效数据 -- ❌ 显示已删除的好友/群聊 - -### 改进后 - -- ✅ 会话列表完整同步,自动清理 -- ✅ 联系人同步包含删除逻辑 -- ✅ 以 API 为准,保持数据一致性 -- ✅ 添加安全检查,防止误删 -- ✅ 批量操作,提高性能 diff --git a/数据库userId修复说明.md b/数据库userId修复说明.md deleted file mode 100644 index 220b0ee..0000000 --- a/数据库userId修复说明.md +++ /dev/null @@ -1,130 +0,0 @@ -# 数据库 userId 字段修复说明 - -## 🐛 问题描述 - -用户登录后看到一堆陌生的好友和会话,数据混乱。 - -## 🔍 根本原因 - -虽然数据库是按 `user.id` 隔离的(如 `CunkebaoDatabase_100`、`CunkebaoDatabase_121`),但每条记录内部还有一个 `userId` 字段用于查询过滤。 - -**问题出在插入数据时使用了错误的 `userId`:** - -### 错误的代码 - -```typescript -// src/store/module/websocket/msgManage.ts (修复前) -const userId = useCustomerStore.getState().currentCustomer?.userId || 0; -``` - -**问题**:`Customer` 接口中**没有 `userId` 字段**,所以这里永远得到 `0`! - -```typescript -// src/store/module/weChat/customer.data.ts -export interface Customer { - id: number; // 这是客服账号 ID,不是登录用户 ID - tenantId: number; - wechatId: string; - // ... 没有 userId 字段! -} -``` - -### 数据流向 - -``` -收到新消息 - ↓ -获取 userId = currentCustomer?.userId || 0 ← 得到 0 - ↓ -插入数据库 CunkebaoDatabase_121 - ├─ 数据库名称:正确 ✅ - └─ 记录 userId 字段:0 ❌ - ↓ -查询数据 - ├─ 从 CunkebaoDatabase_121 查询 ✅ - └─ WHERE userId = 121 ❌ (记录中是 0,查不到!) -``` - -## ✅ 修复方案 - -### 1. 添加正确的导入 - -```typescript -// src/store/module/websocket/msgManage.ts -import { useUserStore } from "../user"; // ← 新增 -``` - -### 2. 修改所有 userId 获取逻辑 - -**修复位置 1:新消息处理** (第 153 行) - -```typescript -// ❌ 修复前 -const userId = useCustomerStore.getState().currentCustomer?.userId || 0; - -// ✅ 修复后 -const userId = useUserStore.getState().user?.id || 0; -``` - -**修复位置 2:好友信息变更** (第 440 行) - -```typescript -// ❌ 修复前 -const userId = useCustomerStore.getState().currentCustomer?.userId || 0; - -// ✅ 修复后 -const userId = useUserStore.getState().user?.id || 0; -``` - -## 📊 两个概念的区别 - -| 概念 | 来源 | 用途 | 示例 | -|------|------|------|------| -| **登录用户 ID** | `useUserStore.user.id` | 数据库隔离、记录归属 | 121 (你的账号) | -| **客服账号 ID** | `useCustomerStore.currentCustomer.id` | 筛选客服微信账号的会话 | 100 (某个微信号) | - -## 🔄 数据修复 - -### 已插入的错误数据 - -之前插入的数据 `userId = 0`,需要清理: - -```typescript -// 可以在浏览器控制台执行 -const userId = 121; // 你的实际 user.id -const db = await indexedDB.open('CunkebaoDatabase_121'); -// 删除 userId = 0 的记录 -await db.chatSessions.where('userId').equals(0).delete(); -await db.contactsUnified.where('userId').equals(0).delete(); -``` - -或者直接删除整个数据库重新同步: - -```typescript -// 浏览器控制台 -indexedDB.deleteDatabase('CunkebaoDatabase_121'); -// 然后刷新页面,会自动重新同步 -``` - -## ✅ 验证修复 - -修复后,新消息应该: - -1. ✅ 插入到正确的数据库(如 `CunkebaoDatabase_121`) -2. ✅ 记录的 `userId` 字段是正确的(如 `121`) -3. ✅ 查询时能正确过滤(`WHERE userId = 121`) -4. ✅ 不会看到其他用户的数据 - -### 检查方法 - -打开浏览器 DevTools → Application → IndexedDB → `CunkebaoDatabase_121` → `chatSessions` - -查看记录的 `userId` 字段,应该是你的登录用户 ID(如 `121`),而不是 `0`。 - -## 📝 总结 - -- **数据库隔离**:通过数据库名称 `CunkebaoDatabase_${userId}` 实现 ✅ -- **记录过滤**:通过记录内的 `userId` 字段实现 ✅ -- **两者必须一致**:数据库名称和记录 userId 都必须使用登录用户的 `user.id` - -修复完成!🎉 diff --git a/数据补齐逻辑修改说明.md b/数据补齐逻辑修改说明.md deleted file mode 100644 index d1e5c7b..0000000 --- a/数据补齐逻辑修改说明.md +++ /dev/null @@ -1,457 +0,0 @@ -# 数据补齐逻辑修改说明 - -## ✅ 已完成的修改 - -### 1. 删除点击会话时的补齐逻辑 - -**文件**: `src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx` - -**修改位置**: 第 1317 行的 `onContactClick` 函数 - -#### 修改前 -```typescript -const onContactClick = async (session: ChatSession) => { - console.log("onContactClick", session); - setCurrentContact(session as any); - - // 标记为已读 - if (session.config.unreadCount > 0) { - // ... - } - - // ❌ 每次点击都获取最新详情并更新数据库(已删除) - (async () => { - let detailResult = await getWechatFriendDetail({ id: session.id }); - // 更新会话数据库、联系人数据库、UI... - })(); -}; -``` - -#### 修改后 -```typescript -const onContactClick = async (session: ChatSession) => { - console.log("onContactClick", session); - setCurrentContact(session as any); - - // 标记为已读 - if (session.config.unreadCount > 0) { - setSessionState(prev => - prev.map(s => - s.id === session.id - ? { ...s, config: { ...s.config, unreadCount: 0 } } - : s, - ), - ); - MessageManager.markAsRead(currentUserId, session.id, session.type); - } - // ✅ 只处理点击和已读,不再请求 API 补齐数据 -}; -``` - -**效果**: -- ✅ 点击会话只设置当前会话 -- ✅ 标记已读状态 -- ✅ 不再每次点击都请求 API -- ✅ 减少不必要的 API 调用 - ---- - -### 2. 修改新消息处理逻辑 - -**文件**: `src/store/module/websocket/msgManage.ts` - -**修改位置**: 第 150-335 行的 `CmdNewMessage` 处理逻辑 - -#### 修改前的逻辑流程 -``` -收到新消息 - ↓ -直接从数据库获取会话 - ↓ -如果会话数据不完整(头像/昵称为空) - ↓ -异步请求 API 补全 - ↓ -插入会话列表 -``` - -**问题**: -- ❌ 先插入会话,后补全数据 -- ❌ 可能在 UI 上短暂显示"未知联系人" -- ❌ 异步补全可能失败,导致数据永久缺失 - -#### 修改后的逻辑流程 -``` -收到新消息 - ↓ -1. 检查联系人是否存在于本地数据库 - ├─ 存在 → 直接使用 - └─ 不存在 → 请求 API 获取详情 - ↓ - 2. 创建联系人数据 - ├─ 基础字段:nickname, avatar, conRemark - ├─ 好友字段:wechatId, alias, gender... - └─ 群聊字段:chatroomId, chatroomOwner... - ↓ - 3. 添加到联系人数据库 - ↓ -4. 从数据库获取会话信息 - ↓ -5. 插入会话列表 -``` - -#### 新逻辑的核心代码 - -```typescript -// 1. 检查联系人是否存在 -const existingContact = await ContactManager.getContactByIdAndType( - userId, - sessionId, - type, -); - -// 2. 如果不存在,先请求 API 补齐数据 -if (!existingContact) { - console.log("⚠️ [新消息] 联系人不存在,先请求 API 补齐数据"); - - try { - // 请求详情 - let detailResult = type === "friend" - ? await getWechatFriendDetail({ id: sessionId }) - : await getWechatChatroomDetail({ id: sessionId }); - - const detail = detailResult?.detail; - if (detail) { - // 创建联系人数据 - const newContact = { - serverId: `${type}_${sessionId}_${wechatAccountId}`, - userId, - id: sessionId, - type, - wechatAccountId: detail.wechatAccountId || wechatAccountId, - nickname: detail.nickname || "", - conRemark: detail.conRemark || "", - avatar: type === "group" - ? detail.chatroomAvatar || "" - : detail.avatar || "", - // ... 其他字段 - }; - - // 添加到数据库 - await ContactManager.addContact(newContact); - console.log("✅ [新消息] 联系人已添加到数据库"); - } - } catch (error) { - console.error("❌ [新消息] 请求 API 补齐数据失败:", error); - } -} - -// 3. 从数据库获取会话信息(此时联系人数据已完整) -const updatedSession = await MessageManager.getSessionByContactId( - userId, - sessionId, - type -); - -// 4. 插入会话列表 -messageStore.addSession(updatedSession); -``` - -**效果**: -- ✅ 先补齐数据,再插入会话列表 -- ✅ UI 显示时数据已完整 -- ✅ 不会出现"未知联系人" -- ✅ 数据完整性更有保障 - ---- - -## 📊 修改对比总结 - -| 项目 | 修改前 | 修改后 | -|------|-------|-------| -| **点击会话** | 每次都请求 API | 不请求 API | -| **新消息处理** | 异步补全数据 | 同步补齐数据后再插入 | -| **数据完整性** | 可能短暂缺失 | 保证完整 | -| **API 调用** | 频繁 | 按需 | -| **用户体验** | 可能看到"未知联系人" | 始终显示完整信息 | - ---- - -## 🔄 完整的数据流向 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ WebSocket 收到新消息 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 1: 检查联系人是否存在于本地数据库 │ -│ ContactManager.getContactByIdAndType() │ -└─────────────────────────────────────────────────────────────┘ - ↓ - [联系人是否存在?] - ╱ ╲ - 是 否 - ↓ ↓ - [跳过补齐] ┌──────────────────────┐ - │ 步骤 2: 请求 API │ - │ • getFriendDetail │ - │ • getGroupDetail │ - └──────────────────────┘ - ↓ - ┌──────────────────────┐ - │ 步骤 3: 创建联系人 │ - │ • 基础字段 │ - │ • 类型特定字段 │ - └──────────────────────┘ - ↓ - ┌──────────────────────┐ - │ 步骤 4: 添加到数据库 │ - │ ContactManager.add │ - └──────────────────────┘ - ↓ - └───────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 5: 从数据库获取会话信息(此时数据已完整) │ -│ MessageManager.getSessionByContactId() │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 6: 插入会话列表 │ -│ • messageStore.addSession() │ -│ • 更新缓存 │ -│ • 发送事件通知 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 7: UI 显示完整的会话信息 │ -│ • 头像 ✅ │ -│ • 昵称 ✅ │ -│ • 备注 ✅ │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## ⚠️ 需要手动替换的代码 - -由于文件较大,自动替换失败,请手动执行以下步骤: - -### 步骤 1: 打开文件 -打开 `src/store/module/websocket/msgManage.ts` - -### 步骤 2: 定位到第 150 行 -找到以下代码: -```typescript -// 更新新架构的SessionStore(增量更新索引和缓存) -try { - const userId = useCustomerStore.getState().currentCustomer?.userId || 0; - if (userId > 0) { - // 从数据库获取更新后的会话信息(带超时保护) - const updatedSession = await Promise.race([ - MessageManager.getSessionByContactId(userId, sessionId, type), - ... -``` - -### 步骤 3: 替换整个 try-catch 块 -将第 150-335 行的整个 `try-catch` 块替换为 `msgManage_new_logic.ts` 中的内容 - -或者直接复制以下代码替换: - -```typescript -// 更新新架构的SessionStore(增量更新索引和缓存) -try { - const userId = useCustomerStore.getState().currentCustomer?.userId || 0; - if (userId > 0) { - // 1. 先检查联系人是否存在于本地数据库 - console.log("🔍 [新消息] 检查联系人是否存在:", { - sessionId, - type, - userId, - }); - - const existingContact = await ContactManager.getContactByIdAndType( - userId, - sessionId, - type, - ); - - // 2. 如果联系人不存在,先请求 API 补齐数据 - if (!existingContact) { - console.log( - "⚠️ [新消息] 联系人不存在,先请求 API 补齐数据:", - { sessionId, type }, - ); - - try { - let detailResult: any = null; - if (type === "friend") { - detailResult = await getWechatFriendDetail({ - id: sessionId, - }); - } else { - detailResult = await getWechatChatroomDetail({ - id: sessionId, - }); - } - - const detail = detailResult?.detail; - if (detail) { - console.log("✅ [新消息] 成功获取详情,创建联系人:", { - id: detail.id, - nickname: detail.nickname, - avatar: detail.avatar || detail.chatroomAvatar, - }); - - // 创建联系人数据 - const newContact: any = { - serverId: `${type}_${sessionId}_${wechatAccountId}`, - userId, - id: sessionId, - type, - wechatAccountId: detail.wechatAccountId || wechatAccountId, - nickname: detail.nickname || "", - conRemark: detail.conRemark || "", - avatar: - type === "group" - ? detail.chatroomAvatar || "" - : detail.avatar || "", - lastUpdateTime: new Date().toISOString(), - sortKey: "", - searchKey: ( - detail.conRemark || - detail.nickname || - "" - ).toLowerCase(), - }; - - // 添加类型特定字段 - if (type === "group") { - Object.assign(newContact, { - chatroomId: detail.chatroomId || "", - chatroomOwner: detail.chatroomOwner || "", - selfDisplayName: - detail.selfDisplyName || detail.selfDisplayName || "", - notice: detail.notice || "", - }); - } else { - Object.assign(newContact, { - wechatFriendId: detail.id, - wechatId: detail.wechatId || "", - alias: detail.alias || "", - gender: detail.gender, - region: detail.region || "", - signature: detail.signature || "", - phone: detail.phone || "", - quanPin: detail.quanPin || "", - groupId: detail.groupId, - }); - } - - // 添加到联系人数据库 - await ContactManager.addContact(newContact); - console.log("✅ [新消息] 联系人已添加到数据库"); - } else { - console.warn("❌ [新消息] API 返回空数据,无法创建联系人"); - } - } catch (error) { - console.error("❌ [新消息] 请求 API 补齐数据失败:", error); - } - } else { - console.log("✅ [新消息] 联系人已存在:", { - id: existingContact.id, - nickname: existingContact.nickname, - avatar: existingContact.avatar ? "有" : "无", - }); - } - - // 3. 从数据库获取或创建会话信息 - const updatedSession = await Promise.race([ - MessageManager.getSessionByContactId(userId, sessionId, type), - new Promise(resolve => - setTimeout(() => resolve(null), 5000), - ), // 5秒超时 - ]); - - if (updatedSession) { - const messageStore = useMessageStore.getState(); - // 增量更新索引 - messageStore.addSession(updatedSession); - // 失效缓存,下次切换账号时会重新计算 - messageStore.invalidateCache(wechatAccountId); - messageStore.invalidateCache(0); // 也失效"全部"的缓存 - - // 更新会话列表缓存(不阻塞主流程) - const cacheKey = `sessions_${wechatAccountId}`; - sessionListCache - .get(cacheKey) - .then(cachedSessions => { - if (cachedSessions) { - // 更新缓存中的会话 - const index = cachedSessions.findIndex( - s => - s.id === updatedSession.id && - s.type === updatedSession.type, - ); - if (index >= 0) { - cachedSessions[index] = updatedSession; - } else { - cachedSessions.push(updatedSession); - } - return sessionListCache.set(cacheKey, cachedSessions); - } - }) - .catch(error => { - console.error("更新会话缓存失败:", error); - }); - } - } -} catch (error) { - console.error("更新SessionStore失败:", error); -} -``` - ---- - -## ✅ 验证修改 - -修改完成后,请验证以下功能: - -### 1. 测试新消息接收 -- [ ] 收到陌生好友的消息,检查是否自动创建联系人 -- [ ] 检查会话列表是否正确显示头像和昵称 -- [ ] 确认不会出现"未知联系人" - -### 2. 测试点击会话 -- [ ] 点击会话能正常打开聊天窗口 -- [ ] 已读状态正常标记 -- [ ] 不会触发额外的 API 请求 - -### 3. 检查日志输出 -``` -收到新消息时应该看到: -🔍 [新消息] 检查联系人是否存在 -✅ [新消息] 联系人已存在(或) -⚠️ [新消息] 联系人不存在,先请求 API 补齐数据 -✅ [新消息] 成功获取详情,创建联系人 -✅ [新消息] 联系人已添加到数据库 -``` - ---- - -## 📝 总结 - -### 修改内容 -1. ✅ 删除点击会话时的补齐逻辑 -2. ✅ 修改新消息处理逻辑为先补齐后插入 -3. ✅ 保证数据完整性 -4. ✅ 减少不必要的 API 调用 - -### 优化效果 -- 🚀 性能提升:减少点击时的 API 调用 -- 💎 数据完整:新消息显示时数据已补齐 -- 👁️ 用户体验:不再看到"未知联系人" -- 🔒 数据一致性:先准备数据再展示 - -修改完成!🎉 diff --git a/未知联系人补全功能说明.md b/未知联系人补全功能说明.md deleted file mode 100644 index 0c375ec..0000000 --- a/未知联系人补全功能说明.md +++ /dev/null @@ -1,552 +0,0 @@ -# 未知联系人补全功能说明 - -## 📋 功能概述 - -当会话列表中出现**未知联系人**(缺少头像、昵称或微信ID)时,系统会自动检测并调用 API 获取完整的好友/群详情,然后更新本地数据库和 UI 显示。 - -## 🔍 检测条件 - -系统会检测以下情况的会话,判定为"需要补全数据": - -```typescript -const needEnrich = sessionsToCheck.filter(s => { - const noName = !s.conRemark && !s.nickname && !s.wechatId; - const isUnknownNickname = s.nickname === "未知联系人"; - const noAvatar = !s.avatar || s.avatar === ""; - - return noName || isUnknownNickname || noAvatar; -}); -``` - -### 检测规则 - -| 条件 | 描述 | 示例 | -|------|------|------| -| **noName** | 备注名、昵称、微信ID 都为空 | `{ conRemark: "", nickname: "", wechatId: "" }` | -| **isUnknownNickname** | 昵称为"未知联系人" | `{ nickname: "未知联系人" }` | -| **noAvatar** | 头像为空或空字符串 | `{ avatar: "" }` | - -只要满足**任一条件**,就会触发补全逻辑。 - ---- - -## 🔄 补全流程 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 1: 检测未知联系人 │ -│ ──────────────────────────────────────────────────────── │ -│ • 遍历所有会话 │ -│ • 筛选出缺少数据的会话 │ -│ • 打印检测日志 │ -└─────────────────────────────────────────────────────────────┘ - ↓ - [是否有需要补全的会话?] - ╱ ╲ - 否 是 - ↓ ↓ - [跳过补全] ┌──────────────────────────┐ - │ 步骤 2: 批量请求 API │ - │ ─────────────────────── │ - │ • 并发控制:每批最多 5 个 │ - │ • 好友 → getFriendDetail │ - │ • 群聊 → getGroupDetail │ - └──────────────────────────┘ - ↓ - ┌──────────────────────────┐ - │ 步骤 3: 更新 UI │ - │ ─────────────────────── │ - │ • setSessionState() │ - │ • 实时显示最新数据 │ - └──────────────────────────┘ - ↓ - ┌──────────────────────────┐ - │ 步骤 4: 更新会话数据库 │ - │ ─────────────────────── │ - │ • MessageManager │ - │ • 持久化到 IndexedDB │ - └──────────────────────────┘ - ↓ - ┌──────────────────────────┐ - │ 步骤 5: 更新联系人数据库 │ - │ ─────────────────────── │ - │ • ContactManager │ - │ • Upsert 逻辑 │ - │ • 方便其他页面使用 │ - └──────────────────────────┘ - ↓ - ┌──────────────────────────┐ - │ 步骤 6: 刷新整体 UI │ - │ ─────────────────────── │ - │ • buildIndexes() │ - │ • switchAccount() │ - └──────────────────────────┘ -``` - ---- - -## 💻 核心代码 - -### 1. API 请求逻辑 - -```typescript -// 根据会话类型调用对应的 API -if (session.type === "friend") { - detailResult = await getWechatFriendDetail({ id: session.id }); -} else { - detailResult = await getWechatChatroomDetail({ id: session.id }); -} - -const detail = detailResult?.detail; -if (!detail) { - console.warn("⚠️ API 返回空数据"); - return; -} -``` - -### 2. 数据更新逻辑 - -```typescript -// 准备更新的数据 -const enrichedData = { - avatar: session.type === "group" - ? detail.chatroomAvatar || session.avatar - : detail.avatar || session.avatar, - nickname: detail.nickname || session.nickname, - conRemark: detail.conRemark || session.conRemark, - wechatId: detail.wechatId || session.wechatId, -}; - -// 1. 实时更新 UI -setSessionState(prev => - prev.map(s => - s.id === session.id && s.type === session.type - ? { ...s, ...enrichedData } - : s, - ), -); - -// 2. 更新会话数据库 -await MessageManager.updateSession({ - userId: currentUserId, - id: session.id, - type: session.type, - ...enrichedData, -}); - -// 3. 更新联系人数据库 -const contactBase = { - serverId: `${session.type}_${session.id}_${detail.wechatAccountId}`, - userId: currentUserId, - id: session.id, - type: session.type, - ...enrichedData, - // ... 其他字段 -}; - -// Upsert 逻辑 -const existContact = await ContactManager.getContactByIdAndType( - currentUserId, - session.id, - session.type, -); - -if (existContact) { - await ContactManager.updateContact(contactBase); -} else { - await ContactManager.addContact(contactBase); -} -``` - -### 3. 并发控制 - -```typescript -// 每次最多处理 5 个,避免并发过高 -const concurrency = 5; -for (let i = 0; i < needEnrich.length; i += concurrency) { - const batch = needEnrich.slice(i, i + concurrency); - - await Promise.all( - batch.map(async session => { - // 处理单个会话 - }), - ); -} -``` - ---- - -## 📊 日志输出 - -### 检测阶段 - -```typescript -// 检测到需要补全的会话 -console.log("🔍 [补全数据] 检测到需要补全的会话:", { - id: 123, - type: "friend", - nickname: "未知联系人", - conRemark: "", - avatar: "无", - 原因: "未知联系人", -}); - -// 开始批量处理 -console.log("🔄 [补全数据] 检测到 5 个会话需要补全数据,开始请求 API..."); -``` - -### API 请求阶段 - -```typescript -// 请求 API -console.log("📡 [补全数据] 请求 friend 详情:", { - id: 123, - 当前昵称: "未知联系人", -}); - -// 成功获取 -console.log("✅ [补全数据] 成功获取详情:", { - id: 123, - type: "friend", - nickname: "张三", - conRemark: "老同学", - avatar: "有", -}); -``` - -### 数据库更新阶段 - -```typescript -// 更新联系人数据库 -console.log("📝 [补全数据] 已更新联系人数据库"); - -// 或添加新联系人 -console.log("➕ [补全数据] 已添加联系人到数据库"); -``` - -### 完成阶段 - -```typescript -// 统计结果 -console.log("✅ [补全数据] 完成:", { - 总数: 5, - 成功: 4, - 失败: 0, - 未找到: 1, -}); - -// 刷新 UI -console.log("🔄 [补全数据] 已刷新 UI,显示最新数据"); -``` - ---- - -## 🎯 使用场景 - -### 场景 1: 新消息来自陌生好友 - -``` -初始状态(WebSocket 新消息): -┌──────────────────────────┐ -│ ID: 123 │ -│ 昵称: "未知联系人" │ -│ 头像: "" │ -│ 来源: WebSocket 新消息 │ -└──────────────────────────┘ - ↓ [自动检测] -┌──────────────────────────┐ -│ 调用 API: │ -│ GET /wechatFriend/123 │ -└──────────────────────────┘ - ↓ [成功获取] -┌──────────────────────────┐ -│ ID: 123 │ -│ 昵称: "张三" │ -│ 备注: "老同学" │ -│ 头像: "https://..." │ -└──────────────────────────┘ - ↓ [更新本地] -┌──────────────────────────┐ -│ ✅ 会话数据库已更新 │ -│ ✅ 联系人数据库已更新 │ -│ ✅ UI 实时显示新数据 │ -└──────────────────────────┘ -``` - -### 场景 2: 历史会话缺少头像 - -``` -同步会话列表后: -┌──────────────────────────┐ -│ 10 个会话 │ -│ - 5 个数据完整 │ -│ - 3 个缺少头像 │ ← 触发补全 -│ - 2 个缺少昵称 │ ← 触发补全 -└──────────────────────────┘ - ↓ -批量请求 API (5 个并发) - ↓ -逐个更新数据库和 UI -``` - -### 场景 3: 群聊改名 - -``` -本地数据: -┌──────────────────────────┐ -│ 群名: "同学聚会群" │ -│ 时间: 2024-01-01 │ -└──────────────────────────┘ - -服务器数据: -┌──────────────────────────┐ -│ 群名: "2024同学聚会" │ ← 已改名 -│ 时间: 2024-01-15 │ -└──────────────────────────┘ - -检测到昵称不一致 → 请求 API → 更新本地 -``` - ---- - -## ⚙️ 配置选项 - -### 并发控制 - -```typescript -const concurrency = 5; // 每批最多处理 5 个 -``` - -**调整建议**: -- **低并发 (3)**: 适合弱网环境,减少 API 压力 -- **中并发 (5)**: 默认值,平衡速度和稳定性 -- **高并发 (10)**: 适合高速网络,加快补全速度 - -### 触发时机 - -当前在以下时机触发: -1. **会话列表同步完成后** (`syncWithServer` 末尾) -2. **组件首次加载时** (如果有缓存数据) - ---- - -## 🛡️ 错误处理 - -### 1. API 请求失败 - -```typescript -catch (error: any) { - console.error("❌ [补全数据] 请求 API 失败:", { - id: session.id, - type: session.type, - error: error?.message || error, - }); - failCount++; - // 继续处理下一个,不中断整体流程 -} -``` - -### 2. API 返回空数据 - -```typescript -if (!detail) { - console.warn("⚠️ [补全数据] API 返回空数据:", { - id: session.id, - type: session.type, - }); - notFoundCount++; - return; // 跳过此会话 -} -``` - -### 3. 数据库更新失败 - -```typescript -try { - await ContactManager.updateContact(contactBase); -} catch (contactError) { - console.error("❌ [补全数据] 更新联系人数据库失败:", contactError); - // 不中断,继续处理其他会话 -} -``` - ---- - -## 📈 性能优化 - -### 1. 批量处理 - -- ✅ 使用 `Promise.all` 并发请求 -- ✅ 每批最多 5 个,避免过载 -- ✅ 失败不影响其他请求 - -### 2. 去重机制 - -```typescript -if (hasEnrichedRef.current) return; // 避免重复执行 -hasEnrichedRef.current = true; -``` - -### 3. 按需更新 - -- ✅ 只更新缺少数据的会话 -- ✅ 完整的会话直接跳过 -- ✅ 减少不必要的 API 调用 - ---- - -## 🔄 与其他功能的联动 - -### 1. WebSocket 新消息补全 - -当 `msgManage.ts` 中收到新消息时: - -```typescript -// msgManage.ts -if (needEnrich) { - // 请求详情 API - const detail = await getWechatFriendDetail({ id }); - // 更新本地数据库 - await MessageManager.updateSession({ ...detail }); - await ContactManager.updateContact({ ...detail }); -} -``` - -### 2. 会话列表同步后补全 - -```typescript -// MessageList/index.tsx -const syncWithServer = async () => { - // 1. 同步会话列表 - await MessageManager.syncSessions(...); - - // 2. 补全未知联系人 - enrichUnknownContacts(); // ← 自动调用 -}; -``` - -### 3. 搜索功能补全 - -```typescript -// SearchAnyone/index.tsx -const handleResultClick = async (item) => { - // 打开会话 - openChat(item); - - // 如果数据不完整,触发补全 - if (!item.avatar || !item.nickname) { - enrichUnknownContacts(); - } -}; -``` - ---- - -## 🎯 预期效果 - -### 用户体验 - -| 操作 | 改进前 | 改进后 | -|------|-------|-------| -| 新消息提醒 | 显示"未知联系人" | 自动显示真实姓名 | -| 会话列表 | 部分头像缺失 | 所有头像自动加载 | -| 搜索结果 | 信息不全 | 完整的联系人信息 | -| 群聊改名 | 显示旧名称 | 自动同步最新名称 | - -### 数据完整性 - -- ✅ **会话表**: 头像、昵称、备注、微信ID 完整 -- ✅ **联系人表**: 所有字段同步更新 -- ✅ **UI 显示**: 实时展示最新数据 - -### 性能表现 - -- ⚡ **并发请求**: 5 个/批,快速完成 -- 💾 **本地缓存**: 减少重复请求 -- 🔄 **增量更新**: 只处理缺失数据 - ---- - -## 🐛 故障排查 - -### 问题 1: 仍然显示"未知联系人" - -**可能原因**: -1. API 返回空数据 -2. 网络请求失败 -3. 数据库更新失败 - -**排查步骤**: -```typescript -// 1. 检查控制台日志 -// 查找 "[补全数据]" 相关日志 - -// 2. 检查 API 返回 -console.log("API 返回:", detailResult); - -// 3. 检查数据库 -const session = await MessageManager.getSessionByContactId(userId, id, type); -console.log("数据库数据:", session); -``` - -### 问题 2: 头像未更新 - -**可能原因**: -1. API 未返回头像字段 -2. 头像字段为空字符串 -3. UI 未刷新 - -**排查步骤**: -```typescript -// 检查 API 返回的头像字段 -console.log("头像:", { - 好友头像: detail.avatar, - 群聊头像: detail.chatroomAvatar, -}); - -// 强制刷新 UI -buildIndexes(updatedSessions); -switchAccount(currentCustomer?.id || 0); -``` - -### 问题 3: 性能慢 - -**可能原因**: -1. 并发数太低 -2. 网络速度慢 -3. 需要补全的数量太多 - -**优化方案**: -```typescript -// 1. 调整并发数 -const concurrency = 10; // 提高到 10 - -// 2. 添加超时控制 -const apiCall = Promise.race([ - getWechatFriendDetail({ id }), - new Promise((_, reject) => - setTimeout(() => reject(new Error('timeout')), 5000) - ) -]); - -// 3. 分批处理 -if (needEnrich.length > 50) { - // 只处理前 50 个,其他延迟处理 -} -``` - ---- - -## ✅ 总结 - -未知联系人补全功能现在能够: - -1. ✅ **自动检测** 缺失数据的会话 -2. ✅ **调用 API** 获取好友/群详情 -3. ✅ **更新数据库** (会话表 + 联系人表) -4. ✅ **实时更新 UI** 展示最新信息 -5. ✅ **并发控制** 提高补全速度 -6. ✅ **错误处理** 失败不影响整体 -7. ✅ **详细日志** 便于调试监控 - -用户不再看到"未知联系人"或空头像,所有会话信息都保持完整和最新!🎉