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. ✅ **详细日志** 便于调试监控 - -用户不再看到"未知联系人"或空头像,所有会话信息都保持完整和最新!🎉