feat: 去掉无用提示词
This commit is contained in:
215
API类型约束使用指南.md
215
API类型约束使用指南.md
@@ -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`*
|
||||
@@ -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,
|
||||
|
||||
206
会话列表修复说明.md
206
会话列表修复说明.md
@@ -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`*
|
||||
220
会话列表问题根本原因和修复.md
220
会话列表问题根本原因和修复.md
@@ -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*
|
||||
245
会话列表问题诊断指南.md
245
会话列表问题诊断指南.md
@@ -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`*
|
||||
@@ -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. 第四阶段:优化性能,添加监控和告警
|
||||
|
||||
实施完成后,代码将更加**简洁、可维护、高效**!🎉
|
||||
203
快速诊断命令.md
203
快速诊断命令.md
@@ -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` 请求的截图或数据
|
||||
|
||||
这些信息将帮助我准确定位问题!
|
||||
|
||||
---
|
||||
|
||||
*提示:如果看到大量日志,可以右键点击控制台选择"保存为..."导出日志文件*
|
||||
397
数据同步功能使用指南.md
397
数据同步功能使用指南.md
@@ -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. **性能优化**: 批量操作,提高效率
|
||||
|
||||
现在本地数据库会**始终与服务器保持一致**,不会再出现"幽灵数据"的问题!🎉
|
||||
454
数据同步机制分析与改进方案.md
454
数据同步机制分析与改进方案.md
@@ -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 为准,保持数据一致性
|
||||
- ✅ 添加安全检查,防止误删
|
||||
- ✅ 批量操作,提高性能
|
||||
130
数据库userId修复说明.md
130
数据库userId修复说明.md
@@ -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`
|
||||
|
||||
修复完成!🎉
|
||||
457
数据补齐逻辑修改说明.md
457
数据补齐逻辑修改说明.md
@@ -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 调用
|
||||
- 💎 数据完整:新消息显示时数据已补齐
|
||||
- 👁️ 用户体验:不再看到"未知联系人"
|
||||
- 🔒 数据一致性:先准备数据再展示
|
||||
|
||||
修改完成!🎉
|
||||
552
未知联系人补全功能说明.md
552
未知联系人补全功能说明.md
@@ -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. ✅ **详细日志** 便于调试监控
|
||||
|
||||
用户不再看到"未知联系人"或空头像,所有会话信息都保持完整和最新!🎉
|
||||
Reference in New Issue
Block a user