refactor(store): 重构ckchat模块为子目录结构
feat(test): 添加数据库测试页面和工具 将ckchat相关代码移动到store/module/ckchat子目录,包含数据定义和实现文件 添加数据库测试页面和工具类,支持服务器ID与本地ID映射 移除不再使用的initSafeArea函数
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useCkChatStore } from "@/store/module/ckchat";
|
||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||
import { Form, Input, Button, Toast, Checkbox } from "antd-mobile";
|
||||
import {
|
||||
EyeInvisibleOutline,
|
||||
|
||||
597
Cunkebao/src/pages/mobile/test/db.tsx
Normal file
597
Cunkebao/src/pages/mobile/test/db.tsx
Normal file
@@ -0,0 +1,597 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Statistic,
|
||||
Row,
|
||||
Col,
|
||||
Alert,
|
||||
Typography,
|
||||
Space,
|
||||
Spin,
|
||||
Modal,
|
||||
message,
|
||||
} from "antd";
|
||||
import {
|
||||
UserOutlined,
|
||||
TeamOutlined,
|
||||
ContactsOutlined,
|
||||
FolderOutlined,
|
||||
PlayCircleOutlined,
|
||||
DeleteOutlined,
|
||||
ReloadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { DatabaseExamples } from "@/utils/db-examples";
|
||||
import {
|
||||
db,
|
||||
kfUserService,
|
||||
groupService,
|
||||
contractService,
|
||||
DatabaseService,
|
||||
} from "@/utils/db";
|
||||
import { testDatabaseUpgrade, resetDatabase } from "@/utils/db-test";
|
||||
import { KfUserListData, GroupData, ContractData } from "@/pages/pc/ckbox/data";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
interface LogEntry {
|
||||
id: number;
|
||||
timestamp: string;
|
||||
type: "info" | "success" | "error";
|
||||
message: string;
|
||||
}
|
||||
|
||||
const DatabaseTestPage: React.FC = () => {
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [dbStats, setDbStats] = useState({
|
||||
kfUsers: 0,
|
||||
groups: 0,
|
||||
contracts: 0,
|
||||
newContactList: 0,
|
||||
});
|
||||
|
||||
const addLog = (type: LogEntry["type"], msg: string) => {
|
||||
const newLog: LogEntry = {
|
||||
id: Date.now(),
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
type,
|
||||
message: msg,
|
||||
};
|
||||
setLogs(prev => [newLog, ...prev].slice(0, 100)); // 保留最新100条日志
|
||||
|
||||
// 使用 Ant Design 的 message 组件显示通知
|
||||
if (type === "success") {
|
||||
message.success(msg);
|
||||
} else if (type === "error") {
|
||||
message.error(msg);
|
||||
} else {
|
||||
message.info(msg);
|
||||
}
|
||||
};
|
||||
|
||||
const updateStats = async () => {
|
||||
try {
|
||||
const newContactListService = new DatabaseService(db.newContractList);
|
||||
const stats = {
|
||||
kfUsers: await kfUserService.count(),
|
||||
groups: await groupService.count(),
|
||||
contracts: await contractService.count(),
|
||||
newContactList: await newContactListService.count(),
|
||||
};
|
||||
setDbStats(stats);
|
||||
} catch (error) {
|
||||
addLog("error", `统计数据更新失败: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
const initDatabase = async () => {
|
||||
try {
|
||||
const result = await testDatabaseUpgrade();
|
||||
if (result.success) {
|
||||
addLog(
|
||||
"success",
|
||||
`数据库初始化成功 - 版本: ${result.version}, 表: ${result.tables?.join(", ")}`,
|
||||
);
|
||||
await updateStats();
|
||||
} else {
|
||||
addLog("error", `数据库初始化失败: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
addLog("error", `数据库初始化失败: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
const runKfUserExample = async () => {
|
||||
setIsRunning(true);
|
||||
addLog("info", "开始客服用户操作示例...");
|
||||
|
||||
try {
|
||||
// 模拟从接口获取的客服用户数据(包含服务器ID)
|
||||
const serverKfUser = {
|
||||
id: Date.now(), // 模拟服务器返回的ID
|
||||
tenantId: 1,
|
||||
wechatId: `test_${Date.now()}`,
|
||||
nickname: "测试客服",
|
||||
alias: "客服小王",
|
||||
avatar: "https://example.com/avatar.jpg",
|
||||
gender: 1,
|
||||
region: "北京",
|
||||
signature: "专业客服,为您服务",
|
||||
bindQQ: "123456789",
|
||||
bindEmail: "test@example.com",
|
||||
bindMobile: "13800138000",
|
||||
createTime: new Date().toISOString(),
|
||||
currentDeviceId: 1,
|
||||
isDeleted: false,
|
||||
deleteTime: "",
|
||||
groupId: 1,
|
||||
memo: "优秀客服",
|
||||
wechatVersion: "8.0.0",
|
||||
labels: ["VIP客服", "专业"],
|
||||
lastUpdateTime: new Date().toISOString(),
|
||||
isOnline: true,
|
||||
};
|
||||
|
||||
const userId = await kfUserService.createWithServerId(serverKfUser);
|
||||
addLog(
|
||||
"success",
|
||||
`创建客服用户成功,本地ID: ${userId}, 服务器ID: ${serverKfUser.id}`,
|
||||
);
|
||||
|
||||
// 查询用户(按本地ID)
|
||||
const user = await kfUserService.findById(userId);
|
||||
addLog("info", `查询到用户: ${user?.nickname}`);
|
||||
|
||||
// 根据服务器ID查询
|
||||
const userByServerId = await kfUserService.findByServerId(
|
||||
serverKfUser.id,
|
||||
);
|
||||
addLog("info", `根据服务器ID查询到用户: ${userByServerId?.nickname}`);
|
||||
|
||||
// 更新用户
|
||||
await kfUserService.update(userId, {
|
||||
nickname: "更新后的昵称",
|
||||
lastUpdateTime: new Date().toISOString(),
|
||||
});
|
||||
addLog("success", "用户信息更新成功");
|
||||
|
||||
await updateStats();
|
||||
} catch (error) {
|
||||
addLog("error", `客服用户操作失败: ${error}`);
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const runGroupExample = async () => {
|
||||
setIsRunning(true);
|
||||
addLog("info", "开始群组操作示例...");
|
||||
|
||||
try {
|
||||
// 模拟从接口获取的群组数据
|
||||
const serverGroup = {
|
||||
id: Date.now(), // 模拟服务器返回的ID
|
||||
wechatAccountId: 1,
|
||||
tenantId: 1,
|
||||
accountId: 1,
|
||||
chatroomId: `chatroom_${Date.now()}`,
|
||||
chatroomOwner: "owner_001",
|
||||
conRemark: "测试群组",
|
||||
nickname: "产品讨论群",
|
||||
chatroomAvatar: "https://example.com/group-avatar.jpg",
|
||||
groupId: 1,
|
||||
config: {
|
||||
chat: true,
|
||||
},
|
||||
unreadCount: 0,
|
||||
notice: "欢迎加入产品讨论群",
|
||||
selfDisplyName: "群管理员",
|
||||
};
|
||||
|
||||
const groupId = await groupService.createWithServerId(serverGroup);
|
||||
addLog(
|
||||
"success",
|
||||
`创建群组成功,本地ID: ${groupId}, 服务器ID: ${serverGroup.id}`,
|
||||
);
|
||||
|
||||
// 更新群组
|
||||
await groupService.update(groupId, {
|
||||
unreadCount: 5,
|
||||
notice: "更新的群公告",
|
||||
});
|
||||
addLog("success", "群组信息更新成功");
|
||||
|
||||
await updateStats();
|
||||
} catch (error) {
|
||||
addLog("error", `群组操作失败: ${error}`);
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const runContractExample = async () => {
|
||||
setIsRunning(true);
|
||||
addLog("info", "开始联系人操作示例...");
|
||||
|
||||
try {
|
||||
// 模拟从接口获取的联系人数据
|
||||
const serverContract = {
|
||||
id: Date.now(), // 模拟服务器返回的ID
|
||||
wechatAccountId: 1,
|
||||
wechatId: `contact_${Date.now()}`,
|
||||
alias: "张三",
|
||||
conRemark: "重要客户",
|
||||
nickname: "张总",
|
||||
quanPin: "zhangsan",
|
||||
avatar: "https://example.com/contact-avatar.jpg",
|
||||
gender: 1,
|
||||
region: "上海",
|
||||
addFrom: 1,
|
||||
phone: "13900139000",
|
||||
labels: ["VIP客户", "重点关注"],
|
||||
signature: "专业人士",
|
||||
accountId: 1,
|
||||
extendFields: null,
|
||||
city: "上海",
|
||||
lastUpdateTime: new Date().toISOString(),
|
||||
isPassed: true,
|
||||
tenantId: 1,
|
||||
groupId: 1,
|
||||
thirdParty: null,
|
||||
additionalPicture: "",
|
||||
desc: "优质客户",
|
||||
config: {
|
||||
chat: true,
|
||||
},
|
||||
lastMessageTime: Date.now(),
|
||||
unreadCount: 0,
|
||||
duplicate: false,
|
||||
};
|
||||
|
||||
const contractId =
|
||||
await contractService.createWithServerId(serverContract);
|
||||
addLog(
|
||||
"success",
|
||||
`创建联系人成功,本地ID: ${contractId}, 服务器ID: ${serverContract.id}`,
|
||||
);
|
||||
|
||||
// 查询联系人
|
||||
const searchResults = await contractService.findWhereStartsWith(
|
||||
"nickname",
|
||||
"张",
|
||||
);
|
||||
addLog("info", `找到姓张的联系人: ${searchResults.length} 个`);
|
||||
|
||||
await updateStats();
|
||||
} catch (error) {
|
||||
addLog("error", `联系人操作失败: ${error}`);
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const runBatchExample = async () => {
|
||||
setIsRunning(true);
|
||||
addLog("info", "开始批量操作示例...");
|
||||
|
||||
try {
|
||||
// 模拟从接口获取的批量联系人数据
|
||||
const batchServerContacts = Array.from({ length: 5 }, (_, i) => ({
|
||||
id: Date.now() + i, // 模拟服务器返回的ID
|
||||
wechatAccountId: 1,
|
||||
wechatId: `batch_${Date.now()}_${i}`,
|
||||
alias: `批量用户${i + 1}`,
|
||||
conRemark: "批量导入",
|
||||
nickname: `用户${i + 1}`,
|
||||
quanPin: `yonghu${i + 1}`,
|
||||
gender: (i % 2) + 1,
|
||||
region: i % 2 === 0 ? "北京" : "上海",
|
||||
addFrom: 3,
|
||||
phone: `1380000000${i}`,
|
||||
labels: ["批量导入"],
|
||||
signature: "",
|
||||
accountId: 1,
|
||||
extendFields: null,
|
||||
lastUpdateTime: new Date().toISOString(),
|
||||
isPassed: true,
|
||||
tenantId: 1,
|
||||
groupId: 1,
|
||||
thirdParty: null,
|
||||
additionalPicture: "",
|
||||
desc: "批量导入的用户",
|
||||
lastMessageTime: Date.now(),
|
||||
unreadCount: 0,
|
||||
duplicate: false,
|
||||
}));
|
||||
|
||||
const batchIds =
|
||||
await contractService.createManyWithServerId(batchServerContacts);
|
||||
addLog(
|
||||
"success",
|
||||
`批量创建联系人成功,创建了 ${batchIds.length} 个联系人`,
|
||||
);
|
||||
|
||||
await updateStats();
|
||||
} catch (error) {
|
||||
addLog("error", `批量操作失败: ${error}`);
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const clearAllData = async () => {
|
||||
Modal.confirm({
|
||||
title: "确认清空数据",
|
||||
content: "确定要清空所有测试数据吗?此操作不可恢复!",
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
setIsRunning(true);
|
||||
addLog("info", "开始清空数据...");
|
||||
try {
|
||||
await db.transaction(
|
||||
"rw",
|
||||
[db.kfUsers, db.groups, db.contracts, db.newContractList],
|
||||
async () => {
|
||||
await db.kfUsers.clear();
|
||||
await db.groups.clear();
|
||||
await db.contracts.clear();
|
||||
await db.newContractList.clear();
|
||||
},
|
||||
);
|
||||
|
||||
addLog("success", "所有数据清空成功");
|
||||
await updateStats();
|
||||
} catch (error) {
|
||||
addLog("error", `清空数据失败: ${error}`);
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const runAllExamples = async () => {
|
||||
setIsRunning(true);
|
||||
addLog("info", "开始运行所有示例...");
|
||||
|
||||
try {
|
||||
await DatabaseExamples.runAllExamples();
|
||||
addLog("success", "所有示例运行完成");
|
||||
await updateStats();
|
||||
} catch (error) {
|
||||
addLog("error", `运行示例失败: ${error}`);
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initDatabase();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ padding: "24px", maxWidth: "1200px", margin: "0 auto" }}>
|
||||
<Title level={2} style={{ textAlign: "center", marginBottom: "32px" }}>
|
||||
数据库操作演示
|
||||
</Title>
|
||||
|
||||
{/* 数据统计 */}
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: "32px" }}>
|
||||
<Col xs={12} sm={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="客服用户"
|
||||
value={dbStats.kfUsers}
|
||||
prefix={<UserOutlined />}
|
||||
valueStyle={{ color: "#1890ff" }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="群组"
|
||||
value={dbStats.groups}
|
||||
prefix={<TeamOutlined />}
|
||||
valueStyle={{ color: "#52c41a" }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="联系人"
|
||||
value={dbStats.contracts}
|
||||
prefix={<ContactsOutlined />}
|
||||
valueStyle={{ color: "#faad14" }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="联系人分组"
|
||||
value={dbStats.newContactList}
|
||||
prefix={<FolderOutlined />}
|
||||
valueStyle={{ color: "#722ed1" }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<Card title="数据库操作" style={{ marginBottom: "24px" }}>
|
||||
<Space wrap size="middle">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<UserOutlined />}
|
||||
onClick={runKfUserExample}
|
||||
loading={isRunning}
|
||||
size="large"
|
||||
>
|
||||
客服用户示例
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<TeamOutlined />}
|
||||
onClick={runGroupExample}
|
||||
loading={isRunning}
|
||||
size="large"
|
||||
style={{ backgroundColor: "#52c41a", borderColor: "#52c41a" }}
|
||||
>
|
||||
群组操作示例
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ContactsOutlined />}
|
||||
onClick={runContractExample}
|
||||
loading={isRunning}
|
||||
size="large"
|
||||
style={{ backgroundColor: "#faad14", borderColor: "#faad14" }}
|
||||
>
|
||||
联系人示例
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<FolderOutlined />}
|
||||
onClick={runBatchExample}
|
||||
loading={isRunning}
|
||||
size="large"
|
||||
style={{ backgroundColor: "#722ed1", borderColor: "#722ed1" }}
|
||||
>
|
||||
批量操作示例
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={runAllExamples}
|
||||
loading={isRunning}
|
||||
size="large"
|
||||
style={{ backgroundColor: "#13c2c2", borderColor: "#13c2c2" }}
|
||||
>
|
||||
运行所有示例
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={clearAllData}
|
||||
loading={isRunning}
|
||||
size="large"
|
||||
>
|
||||
清空所有数据
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={async () => {
|
||||
Modal.confirm({
|
||||
title: "确认重置数据库",
|
||||
content:
|
||||
"确定要重置数据库吗?这将删除所有数据并重新创建数据库!",
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
setIsRunning(true);
|
||||
try {
|
||||
const result = await resetDatabase();
|
||||
if (result.success) {
|
||||
addLog(
|
||||
"success",
|
||||
`数据库重置成功 - 版本: ${result.version}, 表: ${result.tables?.join(", ")}`,
|
||||
);
|
||||
await updateStats();
|
||||
} else {
|
||||
addLog("error", `数据库重置失败: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
addLog("error", `数据库重置失败: ${error}`);
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
}}
|
||||
loading={isRunning}
|
||||
size="large"
|
||||
>
|
||||
重置数据库
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* 运行状态 */}
|
||||
{isRunning && (
|
||||
<Alert
|
||||
message="操作进行中..."
|
||||
type="info"
|
||||
showIcon
|
||||
icon={<Spin />}
|
||||
style={{ marginBottom: "24px" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 日志显示 */}
|
||||
<Card
|
||||
title="操作日志"
|
||||
extra={
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => setLogs([])}
|
||||
size="small"
|
||||
>
|
||||
清空日志
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div style={{ maxHeight: "400px", overflowY: "auto" }}>
|
||||
{logs.length === 0 ? (
|
||||
<div
|
||||
style={{ textAlign: "center", padding: "40px 0", color: "#999" }}
|
||||
>
|
||||
<Text type="secondary">暂无日志</Text>
|
||||
</div>
|
||||
) : (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="small">
|
||||
{logs.map(log => (
|
||||
<Alert
|
||||
key={log.id}
|
||||
message={
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
<span style={{ flex: 1 }}>{log.message}</span>
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{ fontSize: "12px", marginLeft: "8px" }}
|
||||
>
|
||||
{log.timestamp}
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
type={
|
||||
log.type === "success"
|
||||
? "success"
|
||||
: log.type === "error"
|
||||
? "error"
|
||||
: "info"
|
||||
}
|
||||
showIcon
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatabaseTestPage;
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
CheckOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { ContractData } from "@/pages/pc/ckbox/data";
|
||||
import { useCkChatStore } from "@/store/module/ckchat";
|
||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||
import styles from "./Person.module.scss";
|
||||
|
||||
const { Sider } = Layout;
|
||||
|
||||
@@ -40,7 +40,7 @@ import { clearUnreadCount, getMessages } from "@/pages/pc/ckbox/api";
|
||||
import styles from "./ChatWindow.module.scss";
|
||||
import { useWebSocketStore, WebSocketMessage } from "@/store/module/websocket";
|
||||
import { formatWechatTime } from "@/utils/common";
|
||||
import { useCkChatStore } from "@/store/module/ckchat";
|
||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||
import Person from "./components/Person";
|
||||
const { Header, Content, Footer } = Layout;
|
||||
const { TextArea } = Input;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useCallback, useEffect } from "react";
|
||||
import { List, Avatar, Collapse, Button } from "antd";
|
||||
import type { CollapseProps } from "antd";
|
||||
import styles from "./WechatFriends.module.scss";
|
||||
import { useCkChatStore } from "@/store/module/ckchat";
|
||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||
import { ContractData, GroupData } from "@/pages/pc/ckbox/data";
|
||||
|
||||
interface WechatFriendsProps {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ContractData, GroupData } from "@/pages/pc/ckbox/data";
|
||||
import WechatFriends from "./WechatFriends";
|
||||
import MessageList from "./MessageList/index";
|
||||
import styles from "./SidebarMenu.module.scss";
|
||||
import { getChatSessions } from "@/store/module/ckchat";
|
||||
import { getChatSessions } from "@/store/module/ckchat/ckchat";
|
||||
|
||||
interface SidebarMenuProps {
|
||||
contracts: ContractData[] | GroupData[];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { Avatar, Badge, Tooltip } from "antd";
|
||||
import styles from "./VerticalUserList.module.scss";
|
||||
import { useCkChatStore, asyncKfSelected } from "@/store/module/ckchat";
|
||||
import { useCkChatStore, asyncKfSelected } from "@/store/module/ckchat/ckchat";
|
||||
|
||||
import { TeamOutlined } from "@ant-design/icons";
|
||||
const VerticalUserList: React.FC = () => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import SidebarMenu from "./components/SidebarMenu/index";
|
||||
import VerticalUserList from "./components/VerticalUserList";
|
||||
import PageSkeleton from "./components/Skeleton";
|
||||
import styles from "./index.module.scss";
|
||||
import { addChatSession } from "@/store/module/ckchat";
|
||||
import { addChatSession } from "@/store/module/ckchat/ckchat";
|
||||
const { Header, Content, Sider } = Layout;
|
||||
import { chatInitAPIdata } from "./main";
|
||||
import { KfUserListData, GroupData, ContractData } from "@/pages/pc/ckbox/data";
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
asyncContractList,
|
||||
asyncChatSessions,
|
||||
asyncNewContractList,
|
||||
} from "@/store/module/ckchat";
|
||||
} from "@/store/module/ckchat/ckchat";
|
||||
import { useWebSocketStore } from "@/store/module/websocket";
|
||||
|
||||
import {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import SelectTest from "@/pages/mobile/test/select";
|
||||
import TestIndex from "@/pages/mobile/test/index";
|
||||
import UploadTest from "@/pages/mobile/test/upload";
|
||||
import DbTest from "@/pages/mobile/test/db";
|
||||
import UpdateNotificationTest from "@/pages/mobile/test/update-notification";
|
||||
import IframeDebugPage from "@/pages/iframe";
|
||||
import { DEV_FEATURES } from "@/utils/env";
|
||||
@@ -33,6 +34,11 @@ const componentTestRoutes = DEV_FEATURES.SHOW_TEST_PAGES
|
||||
element: <IframeDebugPage />,
|
||||
auth: false, // 不需要认证,方便调试
|
||||
},
|
||||
{
|
||||
path: "/test/db",
|
||||
element: <DbTest />,
|
||||
auth: false, // 不需要认证,方便调试
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createPersistStore } from "@/store/createPersistStore";
|
||||
import { CkChatState, CkUserInfo, CkTenant } from "./ckchat.data";
|
||||
import { CkChatState, CkUserInfo, CkTenant } from "./ckchat/ckchat.data";
|
||||
import {
|
||||
ContractData,
|
||||
GroupData,
|
||||
|
||||
205
Cunkebao/src/store/module/ckchat/ckchat.ts
Normal file
205
Cunkebao/src/store/module/ckchat/ckchat.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { createPersistStore } from "@/store/createPersistStore";
|
||||
import { CkChatState, CkUserInfo, CkTenant } from "./ckchat.data";
|
||||
import {
|
||||
ContractData,
|
||||
GroupData,
|
||||
CkAccount,
|
||||
KfUserListData,
|
||||
} from "@/pages/pc/ckbox/data";
|
||||
|
||||
export const useCkChatStore = createPersistStore<CkChatState>(
|
||||
set => ({
|
||||
userInfo: null,
|
||||
isLoggedIn: false,
|
||||
contractList: [], //联系人列表
|
||||
chatSessions: [], //聊天会话
|
||||
kfUserList: [], //客服列表
|
||||
kfSelected: 0,
|
||||
newContractList: [], //联系人分组
|
||||
kfSelectedUser: () => {
|
||||
const state = useCkChatStore.getState();
|
||||
return state.kfUserList.find(item => item.id === state.kfSelected);
|
||||
},
|
||||
asyncKfSelected: (data: number) => {
|
||||
set({ kfSelected: data });
|
||||
},
|
||||
// 异步设置会话列表
|
||||
asyncNewContractList: data => {
|
||||
set({ newContractList: data });
|
||||
},
|
||||
getNewContractList: () => {
|
||||
const state = useCkChatStore.getState();
|
||||
return state.newContractList;
|
||||
},
|
||||
// 异步设置会话列表
|
||||
asyncChatSessions: data => {
|
||||
set({ chatSessions: data });
|
||||
},
|
||||
// 异步设置联系人列表
|
||||
asyncContractList: data => {
|
||||
set({ contractList: data });
|
||||
},
|
||||
// 控制终端用户列表
|
||||
getkfUserList: () => {
|
||||
const state = useCkChatStore.getState();
|
||||
return state.kfUserList;
|
||||
},
|
||||
asyncKfUserList: data => {
|
||||
set({ kfUserList: data });
|
||||
},
|
||||
// 删除控制终端用户
|
||||
deleteCtrlUser: (userId: number) => {
|
||||
set(state => ({
|
||||
kfUserList: state.kfUserList.filter(item => item.id !== userId),
|
||||
}));
|
||||
},
|
||||
// 更新控制终端用户
|
||||
updateCtrlUser: (user: KfUserListData) => {
|
||||
set(state => ({
|
||||
kfUserList: state.kfUserList.map(item =>
|
||||
item.id === user.id ? user : item,
|
||||
),
|
||||
}));
|
||||
},
|
||||
// 清空控制终端用户列表
|
||||
clearkfUserList: () => {
|
||||
set({ kfUserList: [] });
|
||||
},
|
||||
// 获取聊天会话
|
||||
getChatSessions: () => {
|
||||
const state = useCkChatStore.getState();
|
||||
return state.chatSessions;
|
||||
},
|
||||
// 添加聊天会话
|
||||
addChatSession: (session: ContractData | GroupData) => {
|
||||
set(state => {
|
||||
// 检查是否已存在相同id的会话
|
||||
const exists = state.chatSessions.some(item => item.id === session.id);
|
||||
// 如果已存在则不添加,否则添加到列表中
|
||||
return {
|
||||
chatSessions: exists
|
||||
? state.chatSessions
|
||||
: [...state.chatSessions, session as ContractData | GroupData],
|
||||
};
|
||||
});
|
||||
},
|
||||
// 更新聊天会话
|
||||
updateChatSession: (session: ContractData | GroupData) => {
|
||||
set(state => ({
|
||||
chatSessions: state.chatSessions.map(item =>
|
||||
item.id === session.id ? session : item,
|
||||
),
|
||||
}));
|
||||
},
|
||||
// 删除聊天会话
|
||||
deleteChatSession: (sessionId: string) => {
|
||||
set(state => ({
|
||||
chatSessions: state.chatSessions.filter(item => item.id !== sessionId),
|
||||
}));
|
||||
},
|
||||
// 设置用户信息
|
||||
setUserInfo: (userInfo: CkUserInfo) => {
|
||||
set({ userInfo, isLoggedIn: true });
|
||||
},
|
||||
|
||||
// 清除用户信息
|
||||
clearUserInfo: () => {
|
||||
set({ userInfo: null, isLoggedIn: false });
|
||||
},
|
||||
|
||||
// 更新账户信息
|
||||
updateAccount: (account: Partial<CkAccount>) => {
|
||||
set(state => ({
|
||||
userInfo: state.userInfo
|
||||
? {
|
||||
...state.userInfo,
|
||||
account: { ...state.userInfo.account, ...account },
|
||||
}
|
||||
: null,
|
||||
}));
|
||||
},
|
||||
|
||||
// 更新租户信息
|
||||
updateTenant: (tenant: Partial<CkTenant>) => {
|
||||
set(state => ({
|
||||
userInfo: state.userInfo
|
||||
? {
|
||||
...state.userInfo,
|
||||
tenant: { ...state.userInfo.tenant, ...tenant },
|
||||
}
|
||||
: null,
|
||||
}));
|
||||
},
|
||||
|
||||
// 获取账户ID
|
||||
getAccountId: () => {
|
||||
const state = useCkChatStore.getState();
|
||||
return Number(state.userInfo?.account?.id) || null;
|
||||
},
|
||||
|
||||
// 获取租户ID
|
||||
getTenantId: () => {
|
||||
const state = useCkChatStore.getState();
|
||||
return state.userInfo?.tenant?.id || null;
|
||||
},
|
||||
|
||||
// 获取账户名称
|
||||
getAccountName: () => {
|
||||
const state = useCkChatStore.getState();
|
||||
return (
|
||||
state.userInfo?.account?.realName ||
|
||||
state.userInfo?.account?.userName ||
|
||||
null
|
||||
);
|
||||
},
|
||||
|
||||
// 获取租户名称
|
||||
getTenantName: () => {
|
||||
const state = useCkChatStore.getState();
|
||||
return state.userInfo?.tenant?.name || null;
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "ckchat-store",
|
||||
partialize: state => ({
|
||||
userInfo: state.userInfo,
|
||||
isLoggedIn: state.isLoggedIn,
|
||||
}),
|
||||
onRehydrateStorage: () => state => {
|
||||
// console.log("CkChat store hydrated:", state);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// 导出便捷的获取方法
|
||||
export const getCkAccountId = () => useCkChatStore.getState().getAccountId();
|
||||
export const getCkTenantId = () => useCkChatStore.getState().getTenantId();
|
||||
export const getCkAccountName = () =>
|
||||
useCkChatStore.getState().getAccountName();
|
||||
export const getCkTenantName = () => useCkChatStore.getState().getTenantName();
|
||||
export const getChatSessions = () =>
|
||||
useCkChatStore.getState().getChatSessions();
|
||||
export const addChatSession = (session: ContractData | GroupData) =>
|
||||
useCkChatStore.getState().addChatSession(session);
|
||||
export const updateChatSession = (session: ContractData | GroupData) =>
|
||||
useCkChatStore.getState().updateChatSession(session);
|
||||
export const deleteChatSession = (sessionId: string) =>
|
||||
useCkChatStore.getState().deleteChatSession(sessionId);
|
||||
export const getkfUserList = () => useCkChatStore.getState().kfUserList;
|
||||
export const addCtrlUser = (user: KfUserListData) =>
|
||||
useCkChatStore.getState().addCtrlUser(user);
|
||||
export const deleteCtrlUser = (userId: number) =>
|
||||
useCkChatStore.getState().deleteCtrlUser(userId);
|
||||
export const updateCtrlUser = (user: KfUserListData) =>
|
||||
useCkChatStore.getState().updateCtrlUser(user);
|
||||
export const asyncKfUserList = (data: KfUserListData[]) =>
|
||||
useCkChatStore.getState().asyncKfUserList(data);
|
||||
export const asyncContractList = (data: ContractData[]) =>
|
||||
useCkChatStore.getState().asyncContractList(data);
|
||||
export const asyncChatSessions = (data: ContractData[]) =>
|
||||
useCkChatStore.getState().asyncChatSessions(data);
|
||||
export const asyncNewContractList = (
|
||||
data: { groupName: string; contacts: any[] }[],
|
||||
) => useCkChatStore.getState().asyncNewContractList(data);
|
||||
export const asyncKfSelected = (data: number) =>
|
||||
useCkChatStore.getState().asyncKfSelected(data);
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createPersistStore } from "@/store/createPersistStore";
|
||||
import { Toast } from "antd-mobile";
|
||||
import { useUserStore } from "./user";
|
||||
import { useCkChatStore } from "@/store/module/ckchat";
|
||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||
const { getAccountId } = useCkChatStore.getState();
|
||||
|
||||
// WebSocket消息类型
|
||||
|
||||
@@ -113,9 +113,3 @@ export function getSafeAreaHeight() {
|
||||
// 3. 默认值
|
||||
return 0;
|
||||
}
|
||||
// 设置全局 CSS 变量
|
||||
export function initSafeArea() {
|
||||
const root = document.documentElement;
|
||||
const height = getSafeAreaHeight();
|
||||
root.style.setProperty("--safe-area-top", `${height}px`);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
146
Cunkebao/src/utils/db-test.ts
Normal file
146
Cunkebao/src/utils/db-test.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 数据库版本升级测试脚本
|
||||
* 用于验证数据库版本升级逻辑是否正常工作
|
||||
*/
|
||||
|
||||
import { db } from "./db";
|
||||
|
||||
// 重置数据库(完全删除并重新创建)
|
||||
export async function resetDatabase() {
|
||||
try {
|
||||
console.log("开始重置数据库...");
|
||||
|
||||
// 关闭数据库连接
|
||||
if (db.isOpen()) {
|
||||
db.close();
|
||||
}
|
||||
|
||||
// 删除数据库
|
||||
await db.delete();
|
||||
console.log("旧数据库已删除");
|
||||
|
||||
// 重新打开数据库(这会创建新的数据库)
|
||||
await db.open();
|
||||
|
||||
console.log("数据库重置成功!");
|
||||
console.log("当前数据库版本:", db.verno);
|
||||
console.log("数据库名称:", db.name);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
version: db.verno,
|
||||
tables: db.tables.map(table => table.name),
|
||||
message: "数据库重置成功",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("数据库重置失败:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
message: "数据库重置失败",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 测试数据库初始化和版本升级
|
||||
export async function testDatabaseUpgrade() {
|
||||
try {
|
||||
console.log("开始测试数据库初始化...");
|
||||
|
||||
// 首先尝试正常打开数据库
|
||||
try {
|
||||
await db.open();
|
||||
} catch (upgradeError) {
|
||||
// 如果遇到升级错误,尝试重置数据库
|
||||
if (
|
||||
upgradeError.message &&
|
||||
upgradeError.message.includes("primary key")
|
||||
) {
|
||||
console.log("检测到主键冲突,尝试重置数据库...");
|
||||
const resetResult = await resetDatabase();
|
||||
if (!resetResult.success) {
|
||||
throw new Error(`数据库重置失败: ${resetResult.error}`);
|
||||
}
|
||||
} else {
|
||||
throw upgradeError;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("数据库初始化成功!");
|
||||
console.log("当前数据库版本:", db.verno);
|
||||
console.log("数据库名称:", db.name);
|
||||
|
||||
// 检查表是否存在
|
||||
const tables = db.tables.map(table => table.name);
|
||||
console.log("数据库表:", tables);
|
||||
|
||||
// 测试基本操作
|
||||
const testData = {
|
||||
tenantId: 1, // 修正为number类型
|
||||
wechatId: "test-wechat-id",
|
||||
nickname: "测试用户",
|
||||
alias: "测试别名",
|
||||
};
|
||||
|
||||
// 测试创建数据
|
||||
const userId = await db.kfUsers.add({
|
||||
...testData,
|
||||
id: 0, // 添加必需的id字段
|
||||
currentDeviceId: 0,
|
||||
isDeleted: false,
|
||||
deleteTime: "",
|
||||
groupId: 0,
|
||||
memo: "", // 备注信息
|
||||
wechatVersion: "",
|
||||
labels: [],
|
||||
lastUpdateTime: new Date().toISOString(), // 修复语法错误,使用字符串类型
|
||||
serverId: "test-server-id-001", // 提供有意义的测试值
|
||||
// 移除不属于KfUserListData接口的字段
|
||||
signature: "",
|
||||
bindQQ: "",
|
||||
bindEmail: "",
|
||||
bindMobile: "",
|
||||
bindWeixin: "",
|
||||
bindAlipay: "",
|
||||
bindTaobao: "",
|
||||
bindJd: "",
|
||||
bindDouyin: "",
|
||||
bindKuaishou: "",
|
||||
bindBilibili: "",
|
||||
avatar: "",
|
||||
gender: 0,
|
||||
region: "",
|
||||
createTime: new Date().toISOString(), // 使用字符串类型
|
||||
});
|
||||
console.log("创建测试用户成功,ID:", userId);
|
||||
|
||||
// 测试查询数据
|
||||
const user = await db.kfUsers.get(userId);
|
||||
console.log("查询测试用户:", user);
|
||||
|
||||
// 清理测试数据
|
||||
await db.kfUsers.delete(userId);
|
||||
console.log("清理测试数据完成");
|
||||
|
||||
return {
|
||||
success: true,
|
||||
version: db.verno,
|
||||
tables: tables,
|
||||
message: "数据库版本升级测试通过",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("数据库测试失败:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
message: "数据库版本升级测试失败",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 如果直接运行此文件,执行测试
|
||||
if (typeof window === "undefined") {
|
||||
testDatabaseUpgrade().then(result => {
|
||||
console.log("测试结果:", result);
|
||||
});
|
||||
}
|
||||
@@ -1,102 +1,111 @@
|
||||
/**
|
||||
* 数据库工具类 - 解决服务器ID与本地自增主键冲突问题
|
||||
*
|
||||
* 问题描述:
|
||||
* 接口返回的数据包含id字段,直接存储到数据库会与Dexie的自增主键(++id)产生冲突
|
||||
*
|
||||
* 解决方案:
|
||||
* 1. 将服务器返回的id字段映射为serverId字段存储
|
||||
* 2. 数据库使用自增的id作为主键
|
||||
* 3. 提供专门的方法处理服务器数据的存储和查询
|
||||
*
|
||||
* 使用方法:
|
||||
* - 存储接口数据:使用 createWithServerId() 或 createManyWithServerId()
|
||||
* - 查询服务器数据:使用 findByServerId()
|
||||
* - 常规操作:使用原有的 create(), findById() 等方法
|
||||
*
|
||||
* 示例:
|
||||
* const serverData = { id: 1001, name: '测试', ... }; // 接口返回的数据
|
||||
* const localId = await service.createWithServerId(serverData); // 存储,返回本地ID
|
||||
* const data = await service.findByServerId(1001); // 根据服务器ID查询
|
||||
*/
|
||||
|
||||
import Dexie, { Table } from "dexie";
|
||||
import { KfUserListData, GroupData, ContractData } from "@/pages/pc/ckbox/data";
|
||||
|
||||
// 定义数据库表结构接口
|
||||
export interface BaseEntity {
|
||||
// 扩展数据类型,添加serverId字段
|
||||
export interface KfUserWithServerId extends KfUserListData {
|
||||
serverId?: number | string; // 服务器返回的原始ID
|
||||
}
|
||||
|
||||
export interface GroupWithServerId extends GroupData {
|
||||
serverId?: number | string; // 服务器返回的原始ID
|
||||
}
|
||||
|
||||
export interface ContractWithServerId extends ContractData {
|
||||
serverId?: number | string; // 服务器返回的原始ID
|
||||
}
|
||||
|
||||
// 新联系人列表数据接口
|
||||
export interface NewContactListData {
|
||||
id?: number;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface User extends BaseEntity {
|
||||
name: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
status: "active" | "inactive";
|
||||
}
|
||||
|
||||
export interface Message extends BaseEntity {
|
||||
userId: number;
|
||||
content: string;
|
||||
type: "text" | "image" | "file";
|
||||
isRead: boolean;
|
||||
}
|
||||
|
||||
export interface ChatRoom extends BaseEntity {
|
||||
name: string;
|
||||
description?: string;
|
||||
memberIds: number[];
|
||||
lastMessageAt?: Date;
|
||||
}
|
||||
|
||||
export interface Setting extends BaseEntity {
|
||||
key: string;
|
||||
value: any;
|
||||
category: string;
|
||||
serverId?: number | string; // 服务器返回的原始ID
|
||||
groupName: string;
|
||||
contacts: ContractData[] | GroupData[];
|
||||
}
|
||||
|
||||
// 数据库类
|
||||
class AppDatabase extends Dexie {
|
||||
users!: Table<User>;
|
||||
messages!: Table<Message>;
|
||||
chatRooms!: Table<ChatRoom>;
|
||||
settings!: Table<Setting>;
|
||||
class CunkebaoDatabase extends Dexie {
|
||||
kfUsers!: Table<KfUserWithServerId>;
|
||||
groups!: Table<GroupWithServerId>;
|
||||
contracts!: Table<ContractWithServerId>;
|
||||
newContractList!: Table<NewContactListData>;
|
||||
|
||||
constructor() {
|
||||
super("CunkebaoDatabase");
|
||||
|
||||
// 版本1:初始版本(不包含serverId字段)
|
||||
this.version(1).stores({
|
||||
users: "++id, name, email, status, createdAt",
|
||||
messages: "++id, userId, type, isRead, createdAt",
|
||||
chatRooms: "++id, name, lastMessageAt, createdAt",
|
||||
settings: "++id, key, category, createdAt",
|
||||
kfUsers:
|
||||
"++id, tenantId, wechatId, nickname, alias, avatar, gender, region, signature, bindQQ, bindEmail, bindMobile, createTime, currentDeviceId, isDeleted, deleteTime, groupId, memo, wechatVersion, lastUpdateTime, isOnline",
|
||||
groups:
|
||||
"++id, wechatAccountId, tenantId, accountId, chatroomId, chatroomOwner, conRemark, nickname, chatroomAvatar, groupId, config, unreadCount, notice, selfDisplyName",
|
||||
contracts:
|
||||
"++id, wechatAccountId, wechatId, alias, conRemark, nickname, quanPin, avatar, gender, region, addFrom, phone, signature, accountId, extendFields, city, lastUpdateTime, isPassed, tenantId, groupId, thirdParty, additionalPicture, desc, config, lastMessageTime, unreadCount, duplicate",
|
||||
newContractList: "++id, groupName, contacts",
|
||||
});
|
||||
|
||||
// 自动添加时间戳
|
||||
this.users.hook("creating", (primKey, obj, trans) => {
|
||||
obj.createdAt = new Date();
|
||||
obj.updatedAt = new Date();
|
||||
});
|
||||
|
||||
this.users.hook("updating", (modifications, primKey, obj, trans) => {
|
||||
modifications.updatedAt = new Date();
|
||||
});
|
||||
|
||||
this.messages.hook("creating", (primKey, obj, trans) => {
|
||||
obj.createdAt = new Date();
|
||||
obj.updatedAt = new Date();
|
||||
});
|
||||
|
||||
this.chatRooms.hook("creating", (primKey, obj, trans) => {
|
||||
obj.createdAt = new Date();
|
||||
obj.updatedAt = new Date();
|
||||
});
|
||||
|
||||
this.settings.hook("creating", (primKey, obj, trans) => {
|
||||
obj.createdAt = new Date();
|
||||
obj.updatedAt = new Date();
|
||||
});
|
||||
// 版本2:添加serverId字段支持
|
||||
this.version(2)
|
||||
.stores({
|
||||
kfUsers:
|
||||
"++id, serverId, tenantId, wechatId, nickname, alias, avatar, gender, region, signature, bindQQ, bindEmail, bindMobile, createTime, currentDeviceId, isDeleted, deleteTime, groupId, memo, wechatVersion, lastUpdateTime, isOnline",
|
||||
groups:
|
||||
"++id, serverId, wechatAccountId, tenantId, accountId, chatroomId, chatroomOwner, conRemark, nickname, chatroomAvatar, groupId, config, unreadCount, notice, selfDisplyName",
|
||||
contracts:
|
||||
"++id, serverId, wechatAccountId, wechatId, alias, conRemark, nickname, quanPin, avatar, gender, region, addFrom, phone, signature, accountId, extendFields, city, lastUpdateTime, isPassed, tenantId, groupId, thirdParty, additionalPicture, desc, config, lastMessageTime, unreadCount, duplicate",
|
||||
newContractList: "++id, serverId, groupName, contacts",
|
||||
})
|
||||
.upgrade(tx => {
|
||||
// 数据库升级逻辑:为现有数据添加serverId字段(可选)
|
||||
console.log("数据库升级到版本2:添加serverId字段支持");
|
||||
// 注意:这里不需要迁移数据,因为serverId是可选字段
|
||||
// 如果需要迁移现有数据,可以在这里添加相应逻辑
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建数据库实例
|
||||
export const db = new AppDatabase();
|
||||
export const db = new CunkebaoDatabase();
|
||||
|
||||
// 通用数据库操作类
|
||||
export class DatabaseService<T extends BaseEntity> {
|
||||
// 简单的数据库操作类
|
||||
export class DatabaseService<T> {
|
||||
constructor(private table: Table<T>) {}
|
||||
|
||||
// 基础 CRUD 操作
|
||||
async create(
|
||||
data: Omit<T, "id" | "createdAt" | "updatedAt">,
|
||||
): Promise<number> {
|
||||
async create(data: Omit<T, "id">): Promise<number> {
|
||||
return await this.table.add(data as T);
|
||||
}
|
||||
|
||||
async createMany(
|
||||
dataList: Omit<T, "id" | "createdAt" | "updatedAt">[],
|
||||
): Promise<number[]> {
|
||||
return await this.table.bulkAdd(dataList as T[], { allKeys: true });
|
||||
// 创建数据(处理服务器ID映射)
|
||||
// 用于存储从接口获取的数据,将服务器的id字段映射为serverId,避免与数据库自增主键冲突
|
||||
async createWithServerId(data: any): Promise<number> {
|
||||
const { id, ...restData } = data;
|
||||
const dataToInsert = {
|
||||
...restData,
|
||||
serverId: id, // 将服务器的id映射为serverId
|
||||
};
|
||||
return await this.table.add(dataToInsert as T);
|
||||
}
|
||||
|
||||
async findById(id: number): Promise<T | undefined> {
|
||||
@@ -107,49 +116,46 @@ export class DatabaseService<T extends BaseEntity> {
|
||||
return await this.table.toArray();
|
||||
}
|
||||
|
||||
async findByIds(ids: number[]): Promise<T[]> {
|
||||
return await this.table.where("id").anyOf(ids).toArray();
|
||||
}
|
||||
|
||||
async update(
|
||||
id: number,
|
||||
data: Partial<Omit<T, "id" | "createdAt">>,
|
||||
): Promise<number> {
|
||||
return await this.table.update(id, data);
|
||||
async update(id: number, data: Partial<T>): Promise<number> {
|
||||
return await this.table.update(id, data as any);
|
||||
}
|
||||
|
||||
async updateMany(
|
||||
updates: { id: number; data: Partial<Omit<T, "id" | "createdAt">> }[],
|
||||
dataList: { id: number; data: Partial<T> }[],
|
||||
): Promise<number> {
|
||||
return await this.table.bulkUpdate(
|
||||
updates.map(u => ({ key: u.id, changes: u.data })),
|
||||
dataList.map(item => ({
|
||||
key: item.id,
|
||||
changes: item.data as any,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
async createMany(dataList: Omit<T, "id">[]): Promise<number[]> {
|
||||
return await this.table.bulkAdd(dataList as T[], { allKeys: true });
|
||||
}
|
||||
|
||||
// 批量创建数据(处理服务器ID映射)
|
||||
// 用于批量存储从接口获取的数据,将服务器的id字段映射为serverId
|
||||
async createManyWithServerId(dataList: any[]): Promise<number[]> {
|
||||
const processedData = dataList.map(item => {
|
||||
const { id, ...restData } = item;
|
||||
return {
|
||||
...restData,
|
||||
serverId: id, // 将服务器的id映射为serverId
|
||||
};
|
||||
});
|
||||
return await this.table.bulkAdd(processedData as T[], { allKeys: true });
|
||||
}
|
||||
|
||||
async delete(id: number): Promise<void> {
|
||||
await this.table.delete(id);
|
||||
}
|
||||
|
||||
async deleteMany(ids: number[]): Promise<void> {
|
||||
await this.table.bulkDelete(ids);
|
||||
}
|
||||
|
||||
async deleteAll(): Promise<void> {
|
||||
async clear(): Promise<void> {
|
||||
await this.table.clear();
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
async paginate(
|
||||
page: number = 1,
|
||||
limit: number = 10,
|
||||
): Promise<{ data: T[]; total: number; page: number; limit: number }> {
|
||||
const offset = (page - 1) * limit;
|
||||
const total = await this.table.count();
|
||||
const data = await this.table.offset(offset).limit(limit).toArray();
|
||||
|
||||
return { data, total, page, limit };
|
||||
}
|
||||
|
||||
// 条件查询
|
||||
async findWhere(field: keyof T, value: any): Promise<T[]> {
|
||||
return await this.table
|
||||
@@ -158,6 +164,13 @@ export class DatabaseService<T extends BaseEntity> {
|
||||
.toArray();
|
||||
}
|
||||
|
||||
// 根据服务器ID查询
|
||||
// 用于根据原始的服务器ID查找数据
|
||||
async findByServerId(serverId: any): Promise<T | undefined> {
|
||||
return await this.table.where("serverId").equals(serverId).first();
|
||||
}
|
||||
|
||||
// 多值查询(IN 查询)
|
||||
async findWhereIn(field: keyof T, values: any[]): Promise<T[]> {
|
||||
return await this.table
|
||||
.where(field as string)
|
||||
@@ -165,6 +178,23 @@ export class DatabaseService<T extends BaseEntity> {
|
||||
.toArray();
|
||||
}
|
||||
|
||||
// 范围查询
|
||||
async findWhereBetween(field: keyof T, min: any, max: any): Promise<T[]> {
|
||||
return await this.table
|
||||
.where(field as string)
|
||||
.between(min, max)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
// 模糊查询(以指定字符串开头)
|
||||
async findWhereStartsWith(field: keyof T, prefix: string): Promise<T[]> {
|
||||
return await this.table
|
||||
.where(field as string)
|
||||
.startsWith(prefix)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
// 不等于查询
|
||||
async findWhereNot(field: keyof T, value: any): Promise<T[]> {
|
||||
return await this.table
|
||||
.where(field as string)
|
||||
@@ -172,6 +202,68 @@ export class DatabaseService<T extends BaseEntity> {
|
||||
.toArray();
|
||||
}
|
||||
|
||||
// 大于查询
|
||||
async findWhereGreaterThan(field: keyof T, value: any): Promise<T[]> {
|
||||
return await this.table
|
||||
.where(field as string)
|
||||
.above(value)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
// 小于查询
|
||||
async findWhereLessThan(field: keyof T, value: any): Promise<T[]> {
|
||||
return await this.table
|
||||
.where(field as string)
|
||||
.below(value)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
// 复合条件查询
|
||||
async findWhereMultiple(
|
||||
conditions: {
|
||||
field: keyof T;
|
||||
operator: "equals" | "above" | "below" | "startsWith";
|
||||
value: any;
|
||||
}[],
|
||||
): Promise<T[]> {
|
||||
let collection = this.table.toCollection();
|
||||
|
||||
for (const condition of conditions) {
|
||||
const { field, operator, value } = condition;
|
||||
collection = collection.and(item => {
|
||||
const fieldValue = (item as any)[field];
|
||||
switch (operator) {
|
||||
case "equals":
|
||||
return fieldValue === value;
|
||||
case "above":
|
||||
return fieldValue > value;
|
||||
case "below":
|
||||
return fieldValue < value;
|
||||
case "startsWith":
|
||||
return (
|
||||
typeof fieldValue === "string" && fieldValue.startsWith(value)
|
||||
);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return await collection.toArray();
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
async findWithPagination(
|
||||
page: number = 1,
|
||||
limit: number = 10,
|
||||
): Promise<{ data: T[]; total: number; page: number; limit: number }> {
|
||||
const offset = (page - 1) * limit;
|
||||
const total = await this.table.count();
|
||||
const data = await this.table.offset(offset).limit(limit).toArray();
|
||||
|
||||
return { data, total, page, limit };
|
||||
}
|
||||
|
||||
// 排序查询
|
||||
async findAllSorted(
|
||||
field: keyof T,
|
||||
@@ -183,264 +275,24 @@ export class DatabaseService<T extends BaseEntity> {
|
||||
: await collection.toArray();
|
||||
}
|
||||
|
||||
// 搜索功能
|
||||
async search(field: keyof T, keyword: string): Promise<T[]> {
|
||||
return await this.table
|
||||
.where(field as string)
|
||||
.startsWithIgnoreCase(keyword)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
// 统计功能
|
||||
// 统计
|
||||
async count(): Promise<number> {
|
||||
return await this.table.count();
|
||||
}
|
||||
|
||||
// 条件统计
|
||||
async countWhere(field: keyof T, value: any): Promise<number> {
|
||||
return await this.table
|
||||
.where(field as string)
|
||||
.equals(value)
|
||||
.count();
|
||||
}
|
||||
|
||||
// 存在性检查
|
||||
async exists(id: number): Promise<boolean> {
|
||||
const item = await this.table.get(id);
|
||||
return !!item;
|
||||
}
|
||||
|
||||
async existsWhere(field: keyof T, value: any): Promise<boolean> {
|
||||
const count = await this.table
|
||||
.where(field as string)
|
||||
.equals(value)
|
||||
.count();
|
||||
return count > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建各表的服务实例
|
||||
export const userService = new DatabaseService(db.users);
|
||||
export const messageService = new DatabaseService(db.messages);
|
||||
export const chatRoomService = new DatabaseService(db.chatRooms);
|
||||
export const settingService = new DatabaseService(db.settings);
|
||||
|
||||
// 专门的业务方法
|
||||
export class UserService extends DatabaseService<User> {
|
||||
constructor() {
|
||||
super(db.users);
|
||||
}
|
||||
|
||||
async findByEmail(email: string): Promise<User | undefined> {
|
||||
return await db.users.where("email").equals(email).first();
|
||||
}
|
||||
|
||||
async findActiveUsers(): Promise<User[]> {
|
||||
return await db.users.where("status").equals("active").toArray();
|
||||
}
|
||||
|
||||
async searchByName(name: string): Promise<User[]> {
|
||||
return await db.users.where("name").startsWithIgnoreCase(name).toArray();
|
||||
}
|
||||
|
||||
async updateStatus(
|
||||
id: number,
|
||||
status: "active" | "inactive",
|
||||
): Promise<number> {
|
||||
return await this.update(id, { status });
|
||||
}
|
||||
}
|
||||
|
||||
export class MessageService extends DatabaseService<Message> {
|
||||
constructor() {
|
||||
super(db.messages);
|
||||
}
|
||||
|
||||
async findByUserId(userId: number): Promise<Message[]> {
|
||||
return await db.messages.where("userId").equals(userId).toArray();
|
||||
}
|
||||
|
||||
async findUnreadMessages(): Promise<Message[]> {
|
||||
return await db.messages.where("isRead").equals(false).toArray();
|
||||
}
|
||||
|
||||
async markAsRead(id: number): Promise<number> {
|
||||
return await this.update(id, { isRead: true });
|
||||
}
|
||||
|
||||
async markAllAsRead(userId: number): Promise<number> {
|
||||
const messages = await db.messages
|
||||
.where("userId")
|
||||
.equals(userId)
|
||||
.and(msg => !msg.isRead)
|
||||
.toArray();
|
||||
const updates = messages.map(msg => ({
|
||||
id: msg.id!,
|
||||
data: { isRead: true },
|
||||
}));
|
||||
return await this.updateMany(updates);
|
||||
}
|
||||
|
||||
async getRecentMessages(limit: number = 50): Promise<Message[]> {
|
||||
return await db.messages
|
||||
.orderBy("createdAt")
|
||||
.reverse()
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
}
|
||||
|
||||
export class SettingService extends DatabaseService<Setting> {
|
||||
constructor() {
|
||||
super(db.settings);
|
||||
}
|
||||
|
||||
async getSetting(key: string): Promise<any> {
|
||||
const setting = await db.settings.where("key").equals(key).first();
|
||||
return setting?.value;
|
||||
}
|
||||
|
||||
async setSetting(
|
||||
key: string,
|
||||
value: any,
|
||||
category: string = "general",
|
||||
): Promise<number> {
|
||||
const existing = await db.settings.where("key").equals(key).first();
|
||||
if (existing) {
|
||||
return await this.update(existing.id!, { value });
|
||||
} else {
|
||||
return await this.create({ key, value, category });
|
||||
}
|
||||
}
|
||||
|
||||
async getSettingsByCategory(category: string): Promise<Setting[]> {
|
||||
return await db.settings.where("category").equals(category).toArray();
|
||||
}
|
||||
|
||||
async deleteSetting(key: string): Promise<void> {
|
||||
await db.settings.where("key").equals(key).delete();
|
||||
}
|
||||
}
|
||||
|
||||
// 数据库工具类
|
||||
export class DatabaseUtils {
|
||||
// 数据导出
|
||||
static async exportData(): Promise<string> {
|
||||
const data = {
|
||||
users: await db.users.toArray(),
|
||||
messages: await db.messages.toArray(),
|
||||
chatRooms: await db.chatRooms.toArray(),
|
||||
settings: await db.settings.toArray(),
|
||||
exportedAt: new Date().toISOString(),
|
||||
};
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
// 数据导入
|
||||
static async importData(jsonData: string): Promise<void> {
|
||||
try {
|
||||
const data = JSON.parse(jsonData);
|
||||
|
||||
await db.transaction(
|
||||
"rw",
|
||||
[db.users, db.messages, db.chatRooms, db.settings],
|
||||
async () => {
|
||||
if (data.users) await db.users.bulkPut(data.users);
|
||||
if (data.messages) await db.messages.bulkPut(data.messages);
|
||||
if (data.chatRooms) await db.chatRooms.bulkPut(data.chatRooms);
|
||||
if (data.settings) await db.settings.bulkPut(data.settings);
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error("导入数据失败: " + error);
|
||||
}
|
||||
}
|
||||
|
||||
// 清空所有数据
|
||||
static async clearAllData(): Promise<void> {
|
||||
await db.transaction(
|
||||
"rw",
|
||||
[db.users, db.messages, db.chatRooms, db.settings],
|
||||
async () => {
|
||||
await db.users.clear();
|
||||
await db.messages.clear();
|
||||
await db.chatRooms.clear();
|
||||
await db.settings.clear();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 获取数据库统计信息
|
||||
static async getStats(): Promise<{
|
||||
users: number;
|
||||
messages: number;
|
||||
chatRooms: number;
|
||||
settings: number;
|
||||
totalSize: number;
|
||||
}> {
|
||||
const [users, messages, chatRooms, settings] = await Promise.all([
|
||||
db.users.count(),
|
||||
db.messages.count(),
|
||||
db.chatRooms.count(),
|
||||
db.settings.count(),
|
||||
]);
|
||||
|
||||
// 估算数据库大小(简单估算)
|
||||
const totalSize = users + messages + chatRooms + settings;
|
||||
|
||||
return { users, messages, chatRooms, settings, totalSize };
|
||||
}
|
||||
|
||||
// 数据库健康检查
|
||||
static async healthCheck(): Promise<{
|
||||
status: "healthy" | "error";
|
||||
message: string;
|
||||
}> {
|
||||
try {
|
||||
await db.users.limit(1).toArray();
|
||||
return { status: "healthy", message: "数据库连接正常" };
|
||||
} catch (error) {
|
||||
return { status: "error", message: "数据库连接异常: " + error };
|
||||
}
|
||||
}
|
||||
|
||||
// 数据备份到文件
|
||||
static async backupToFile(): Promise<void> {
|
||||
const data = await this.exportData();
|
||||
const blob = new Blob([data], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `cunkebao-backup-${new Date().toISOString().split("T")[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// 从文件恢复数据
|
||||
static async restoreFromFile(file: File): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async e => {
|
||||
try {
|
||||
const jsonData = e.target?.result as string;
|
||||
await this.importData(jsonData);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
reader.onerror = () => reject(new Error("文件读取失败"));
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建业务服务实例
|
||||
export const userBusinessService = new UserService();
|
||||
export const messageBusinessService = new MessageService();
|
||||
export const settingBusinessService = new SettingService();
|
||||
export const kfUserService = new DatabaseService(db.kfUsers);
|
||||
export const groupService = new DatabaseService(db.groups);
|
||||
export const contractService = new DatabaseService(db.contracts);
|
||||
|
||||
// 默认导出数据库实例
|
||||
export default db;
|
||||
|
||||
Reference in New Issue
Block a user