feat: 去掉无用提示词

This commit is contained in:
乘风
2026-01-20 10:17:27 +08:00
parent 1d00f02606
commit 25f9c55b76
12 changed files with 120 additions and 3882 deletions

View File

@@ -1,215 +0,0 @@
# API 类型约束使用指南
## 📋 概述
已为 `request``request2` 添加了泛型类型约束支持,可以在编译时提供类型检查,减少运行时错误。
## 🎯 类型定义
### 统一类型定义文件
**文件**: `src/api/types.ts`
定义了以下类型:
```typescript
// 标准 API 响应结构
export interface ApiResponse<T = any> {
code?: number;
success?: boolean;
msg?: string;
message?: string;
data?: T;
list?: T[]; // 列表接口常用字段
total?: number; // 分页接口常用字段
[key: string]: any;
}
// 详情接口响应结构
export interface ApiDetailResponse<T = any> {
code?: number;
success?: boolean;
msg?: string;
message?: string;
detail?: T; // 详情接口常用字段
data?: T;
[key: string]: any;
}
// 分页响应结构
export interface ApiPageResponse<T = any> {
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<User> => {
return request<User>("/api/user", { id }, "GET");
};
// 列表接口
const getUserList = async (): Promise<User[]> => {
return request<User[]>("/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<MessageItem[]> => {
return request<MessageItem[]>("/v1/kefu/message/list", { page: 1, limit: 20 });
};
// 方式2返回完整响应结构如果需要访问 total 等字段)
const getMessagesWithTotal = async (): Promise<ApiResponse<MessageItem[]>> => {
return request<ApiResponse<MessageItem[]>>("/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<ApiDetailResponse<FriendDetail>> => {
return request<ApiDetailResponse<FriendDetail>>("/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<MessageListItem[] | ApiResponse<MessageListItem[]>> {
return request<MessageListItem[] | ApiResponse<MessageListItem[]>>("/v1/kefu/message/list", params, "GET");
}
export const getWechatFriendDetail = (params: { id: number }): Promise<ApiDetailResponse<WechatFriendDetail>> => {
return request<ApiDetailResponse<WechatFriendDetail>>("/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<Item[]>("/api/list");
// 如果需要访问 total使用 ApiResponse
const result = await request<ApiResponse<Item[]>>("/api/list");
const { list, total } = result;
```
2. **详情接口**
```typescript
// 使用 ApiDetailResponse
const result = await request<ApiDetailResponse<Detail>>("/api/detail");
const detail = result.detail;
```
3. **直接数据**
```typescript
// 如果拦截器直接返回数据(不是包装结构)
const data = await request<User>("/api/user");
```
## ✅ 优势
1. **类型安全**:编译时检查,减少运行时错误
2. **IDE 提示**:自动补全和类型提示
3. **重构友好**修改类型定义时TypeScript 会提示所有需要更新的地方
4. **文档化**:类型定义本身就是最好的文档
## 📌 注意事项
1. **向后兼容**:如果不指定泛型,默认返回 `any`,保持向后兼容
2. **响应结构**:需要根据实际 API 响应结构调整类型定义
3. **拦截器处理**:注意 `request.ts` 的拦截器会提取 `payload.data`,所以类型定义要考虑这一点
## 🔄 迁移建议
逐步迁移现有 API 文件:
1. 先定义数据类型接口
2. 为 API 函数添加返回类型
3. 使用泛型约束 `request<T>()`
4. 测试确保类型正确
---
*创建时间2024年*
*相关文件:`src/api/request.ts`, `src/api/request2.ts`, `src/api/types.ts`*

View File

@@ -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,

View File

@@ -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`*

View File

@@ -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*

View File

@@ -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 请求失败:**
- 401Token 过期,需要重新登录
- 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);
});
```
## 🔧 常见问题及解决方案
### 问题 1API 返回空数组
**原因:**
- 用户确实没有会话数据
- API 参数错误
- 服务器过滤了数据
**解决:**
1. 检查 API 请求参数是否正确
2. 确认用户是否有会话数据(联系后端)
3. 检查是否有筛选条件
### 问题 2API 返回非数组格式
**原因:**
- 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`*

View File

@@ -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<EnrichResult> {
// 统一的补全逻辑
// 1. 检查数据是否完整
// 2. 请求 API
// 3. 根据配置更新数据库/Store
// 4. 返回结果
}
/**
* 批量补全
*/
static async enrichContacts(
contacts: Array<{id: number; type: "friend" | "group"}>,
options?: EnrichOptions
): Promise<BatchEnrichResult> {
// 并发控制
// 批量处理
// 统计结果
}
}
```
**使用示例**:
```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<string, Promise<any>>();
private static cache = new Map<string, { data: any; timestamp: number }>();
private static CACHE_TTL = 5 * 60 * 1000; // 5分钟
static async enrichContact(params: EnrichParams): Promise<EnrichResult> {
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<EnrichTask> = new PriorityQueue();
static async enrichContact(
params: EnrichParams,
priority: "high" | "normal" | "low" = "normal"
): Promise<EnrichResult> {
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<EnrichResult> {
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. 第四阶段:优化性能,添加监控和告警
实施完成后,代码将更加**简洁、可维护、高效**!🎉

View File

@@ -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` 请求的截图或数据
这些信息将帮助我准确定位问题!
---
*提示:如果看到大量日志,可以右键点击控制台选择"保存为..."导出日志文件*

View File

@@ -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. **性能优化**: 批量操作提高效率
现在本地数据库会**始终与服务器保持一致**不会再出现"幽灵数据"的问题!🎉

View File

@@ -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<boolean> {
// 如果服务器返回数据为空,但本地有大量数据,可能是 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<void> {
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 为准,保持数据一致性
- ✅ 添加安全检查,防止误删
- ✅ 批量操作,提高性能

View File

@@ -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`
修复完成!🎉

View File

@@ -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<null>(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<ChatSession[]>(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 调用
- 💎 数据完整:新消息显示时数据已补齐
- 👁️ 用户体验:不再看到"未知联系人"
- 🔒 数据一致性:先准备数据再展示
修改完成!🎉

View File

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