feat(db): 添加Dexie数据库支持并重构数据存储结构

添加Dexie作为IndexedDB封装库,实现本地数据存储功能
重构数据接口定义和存储模块结构,优化类型定义
统一数据接口文件位置,增强代码可维护性
This commit is contained in:
超级老白兔
2025-08-29 15:13:31 +08:00
parent bdc94d853d
commit bc7cc6810d
13 changed files with 1051 additions and 82 deletions

View File

@@ -10,6 +10,7 @@
"antd-mobile-icons": "^0.3.0",
"axios": "^1.6.7",
"dayjs": "^1.11.13",
"dexie": "^4.2.0",
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2",
"react": "^18.2.0",

View File

@@ -26,6 +26,9 @@ importers:
dayjs:
specifier: ^1.11.13
version: 1.11.13
dexie:
specifier: ^4.2.0
version: 4.2.0
echarts:
specifier: ^5.6.0
version: 5.6.0
@@ -1064,6 +1067,9 @@ packages:
engines: {node: '>=0.10'}
hasBin: true
dexie@4.2.0:
resolution: {integrity: sha512-OSeyyWOUetDy9oFWeddJgi83OnRA3hSFh3RrbltmPgqHszE9f24eUCVLI4mPg0ifsWk0lQTdnS+jyGNrPMvhDA==}
dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
@@ -3397,6 +3403,8 @@ snapshots:
detect-libc@1.0.3:
optional: true
dexie@4.2.0: {}
dir-glob@3.0.1:
dependencies:
path-type: 4.0.0

View File

@@ -43,16 +43,15 @@ import {
FilePptOutlined,
PlayCircleFilled,
} from "@ant-design/icons";
import { ChatRecord, ContractData } from "@/pages/pc/ckbox/data";
import { ChatRecord, ContractData, GroupData } from "@/pages/pc/ckbox/data";
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";
const { Header, Content, Footer, Sider } = Layout;
const { TextArea } = Input;
interface ChatWindowProps {
contract: ContractData;
contract: ContractData | GroupData;
onSendMessage: (message: string) => void;
showProfile?: boolean;
onToggleProfile?: () => void;
@@ -797,6 +796,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
label: "视频通话",
},
{
key: "divider1",
type: "divider",
},
{
@@ -808,6 +808,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
label: "消息免打扰",
},
{
key: "divider2",
type: "divider",
},
{

View File

@@ -1,13 +1,12 @@
import React from "react";
import { List, Avatar, Badge } from "antd";
import { UserOutlined, TeamOutlined } from "@ant-design/icons";
import dayjs from "dayjs";
import { ContractData, GroupData } from "@/pages/pc/ckbox/data";
import styles from "./MessageList.module.scss";
import { formatWechatTime } from "@/utils/common";
interface MessageListProps {
chatSessions: ContractData[];
currentChat: ContractData;
chatSessions: ContractData[] | GroupData[];
currentChat: ContractData | GroupData;
onChatSelect: (chat: ContractData | GroupData) => void;
}
@@ -19,9 +18,10 @@ const MessageList: React.FC<MessageListProps> = ({
return (
<div className={styles.messageList}>
<List
dataSource={chatSessions}
dataSource={chatSessions as ContractData[]}
renderItem={session => (
<List.Item
key={session.id}
className={`${styles.messageItem} ${
currentChat?.id === session.id ? styles.active : ""
}`}

View File

@@ -1,14 +1,14 @@
import React, { useState, useCallback, useEffect } from "react";
import { List, Avatar, Collapse, Button } from "antd";
import type { CollapseProps } from "antd";
import { ContractData } from "@/pages/pc/ckbox/data";
import styles from "./WechatFriends.module.scss";
import { useCkChatStore } from "@/store/module/ckchat";
import { ContractData, GroupData } from "@/pages/pc/ckbox/data";
interface WechatFriendsProps {
contracts: ContractData[];
onContactClick: (contract: ContractData) => void;
selectedContactId?: ContractData;
contracts: ContractData[] | GroupData[];
onContactClick: (contract: ContractData | GroupData) => void;
selectedContactId?: ContractData | GroupData;
}
const ContactListSimple: React.FC<WechatFriendsProps> = ({
@@ -174,7 +174,7 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
<div className={styles.header}></div>
<List
className={styles.list}
dataSource={contracts}
dataSource={contracts as ContractData[]}
renderItem={renderContactItem}
/>
</>

View File

@@ -6,17 +6,17 @@ import {
ChromeOutlined,
MessageOutlined,
} from "@ant-design/icons";
import { ContractData } from "@/pages/pc/ckbox/data";
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";
interface SidebarMenuProps {
contracts: ContractData[];
currentChat: ContractData;
onContactClick: (contract: ContractData) => void;
onChatSelect: (chat: ContractData) => void;
contracts: ContractData[] | GroupData[];
currentChat: ContractData | GroupData;
onContactClick: (contract: ContractData | GroupData) => void;
onChatSelect: (chat: ContractData | GroupData) => void;
loading?: boolean;
}
@@ -152,7 +152,7 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({
case "contracts":
return (
<WechatFriends
contracts={getFilteredContacts()}
contracts={getFilteredContacts() as ContractData[]}
onContactClick={onContactClick}
selectedContactId={currentChat}
/>

View File

@@ -1,3 +1,45 @@
//终端用户数据接口
export interface KfUserListData {
id: number;
tenantId: number;
wechatId: string;
nickname: string;
alias: string;
avatar: string;
gender: number;
region: string;
signature: string;
bindQQ: string;
bindEmail: string;
bindMobile: string;
createTime: string;
currentDeviceId: number;
isDeleted: boolean;
deleteTime: string;
groupId: number;
memo: string;
wechatVersion: string;
labels: string[];
lastUpdateTime: string;
isOnline?: boolean;
[key: string]: any;
}
// 账户信息接口
export interface CkAccount {
id: number;
realName: string;
nickname: string | null;
memo: string | null;
avatar: string;
userName: string;
secret: string;
accountType: number;
departmentId: number;
useGoogleSecretKey: boolean;
hasVerifyGoogleSecret: boolean;
}
//群聊数据接口
export interface GroupData {
id?: number;
@@ -10,7 +52,9 @@ export interface GroupData {
nickname: string;
chatroomAvatar: string;
groupId: number;
config: any;
config?: {
chat: boolean;
};
unreadCount: number;
notice: string;
selfDisplyName: string;

View File

@@ -2,7 +2,6 @@ import React, { useState, useEffect } from "react";
import { Layout, Button, Space, message, Tooltip } from "antd";
import { InfoCircleOutlined, MessageOutlined } from "@ant-design/icons";
import dayjs from "dayjs";
import { ContractData } from "./data";
import ChatWindow from "./components/ChatWindow/index";
import SidebarMenu from "./components/SidebarMenu/index";
import VerticalUserList from "./components/VerticalUserList";
@@ -11,13 +10,14 @@ import styles from "./index.module.scss";
import { addChatSession } from "@/store/module/ckchat";
const { Header, Content, Sider } = Layout;
import { chatInitAPIdata } from "./main";
import { KfUserListData } from "@/store/module/ckchat.data";
import { KfUserListData, GroupData, ContractData } from "@/pages/pc/ckbox/data";
const CkboxPage: React.FC = () => {
const [messageApi, contextHolder] = message.useMessage();
const [contracts, setContacts] = useState<any[]>([]);
const [currentChat, setCurrentChat] = useState<ContractData | null>(null);
const [activeVerticalUserId, setActiveVerticalUserId] = useState(0);
const [currentChat, setCurrentChat] = useState<ContractData | GroupData>(
null,
);
const [loading, setLoading] = useState(false);
const [showProfile, setShowProfile] = useState(true);
@@ -53,7 +53,7 @@ const CkboxPage: React.FC = () => {
});
}, []);
const handleContactClick = (contract: ContractData) => {
const handleContactClick = (contract: ContractData | GroupData) => {
addChatSession(contract);
setCurrentChat(contract);
};

View File

@@ -16,7 +16,7 @@ import {
} from "./api";
const { sendCommand } = useWebSocketStore.getState();
import { useUserStore } from "@/store/module/user";
import { KfUserListData } from "@/store/module/ckchat.data";
import { ContractData, GroupData, KfUserListData } from "@/pages/pc/ckbox/data";
const { login2 } = useUserStore.getState();
//获取触客宝基础信息
export const chatInitAPIdata = async () => {
@@ -24,11 +24,6 @@ export const chatInitAPIdata = async () => {
//获取联系人列表
const contractList = await getAllContactList();
//构建联系人列表标签
const newContractList = await createContractList(contractList);
// 会话列表分组
asyncNewContractList(newContractList);
//获取联系人列表
asyncContractList(contractList);
@@ -46,6 +41,12 @@ export const chatInitAPIdata = async () => {
//获取群列表
const groupList = await getAllGroupList();
//构建联系人列表标签
const newContractList = await createContractList(contractList, groupList);
console.log("分组信息", newContractList);
// 会话列表分组
asyncNewContractList(newContractList);
//获取消息会话列表并按lastUpdateTime排序
const filterUserSessions = contractList?.filter(
v => v?.config && v.config?.chat,
@@ -92,7 +93,10 @@ export const chatInitAPIdata = async () => {
};
//构建联系人列表标签
export const createContractList = async (contractList: any[]) => {
export const createContractList = async (
contractList: ContractData[],
groupList: GroupData[],
) => {
const LablesRes = await Promise.all(
[1, 2].map(item =>
WechatGroup({
@@ -106,7 +110,7 @@ export const createContractList = async (contractList: any[]) => {
// 根据countLables中的groupName整理contractList数据
// 返回按标签分组的联系人数组,包括未分组标签(在数组最后)
return organizeContactsByLabels(contractList, countLables);
return organizeContactsByLabels(countLables, contractList, groupList);
};
/**
@@ -116,8 +120,9 @@ export const createContractList = async (contractList: any[]) => {
* @returns 按标签分组的联系人
*/
export const organizeContactsByLabels = (
contractList: any[],
countLables: any[],
contractList: ContractData[],
groupList: GroupData[],
) => {
// 创建结果对象,用于存储按标签分组的联系人
const result: { [key: string]: any[] } = {};

View File

@@ -1,46 +1,4 @@
import { ContractData } from "../../pages/pc/ckbox/data";
//终端用户数据接口
export interface KfUserListData {
id: number;
tenantId: number;
wechatId: string;
nickname: string;
alias: string;
avatar: string;
gender: number;
region: string;
signature: string;
bindQQ: string;
bindEmail: string;
bindMobile: string;
createTime: string;
currentDeviceId: number;
isDeleted: boolean;
deleteTime: string;
groupId: number;
memo: string;
wechatVersion: string;
labels: string[];
lastUpdateTime: string;
isOnline?: boolean;
[key: string]: any;
}
// 账户信息接口
export interface CkAccount {
id: number;
realName: string;
nickname: string | null;
memo: string | null;
avatar: string;
userName: string;
secret: string;
accountType: number;
departmentId: number;
useGoogleSecretKey: boolean;
hasVerifyGoogleSecret: boolean;
}
import { ContractData, KfUserListData, CkAccount } from "@/pages/pc/ckbox/data";
// 权限片段接口
export interface PrivilegeFrag {

View File

@@ -1,13 +1,11 @@
import { createPersistStore } from "@/store/createPersistStore";
import { CkChatState, CkUserInfo, CkTenant } from "./ckchat.data";
import {
CkChatState,
CkUserInfo,
ContractData,
GroupData,
CkAccount,
CkTenant,
KfUserListData,
} from "./ckchat.data";
import { ContractData, GroupData } from "@/pages/pc/ckbox/data";
} from "@/pages/pc/ckbox/data";
export const useCkChatStore = createPersistStore<CkChatState>(
set => ({

View File

@@ -0,0 +1,508 @@
// 数据库使用示例
import {
db,
userService,
messageService,
chatRoomService,
settingService,
userBusinessService,
messageBusinessService,
settingBusinessService,
DatabaseUtils,
type User,
type Message,
type ChatRoom,
type Setting,
} from "./db";
// ============= 基础 CRUD 操作示例 =============
// 1. 用户操作示例
export const userExamples = {
// 创建用户
async createUser() {
const userId = await userService.create({
name: "张三",
email: "zhangsan@example.com",
status: "active",
});
console.log("创建用户成功ID:", userId);
return userId;
},
// 批量创建用户
async createMultipleUsers() {
const userIds = await userService.createMany([
{ name: "李四", email: "lisi@example.com", status: "active" },
{ name: "王五", email: "wangwu@example.com", status: "inactive" },
{ name: "赵六", email: "zhaoliu@example.com", status: "active" },
]);
console.log("批量创建用户成功IDs:", userIds);
return userIds;
},
// 查询用户
async findUsers() {
// 查询所有用户
const allUsers = await userService.findAll();
console.log("所有用户:", allUsers);
// 根据ID查询
const user = await userService.findById(1);
console.log("ID为1的用户:", user);
// 条件查询
const activeUsers = await userService.findWhere("status", "active");
console.log("活跃用户:", activeUsers);
return { allUsers, user, activeUsers };
},
// 分页查询
async paginateUsers() {
const result = await userService.paginate(1, 5); // 第1页每页5条
console.log("分页结果:", result);
return result;
},
// 更新用户
async updateUser() {
const count = await userService.update(1, {
name: "张三(已更新)",
status: "inactive",
});
console.log("更新用户数量:", count);
return count;
},
// 删除用户
async deleteUser() {
await userService.delete(1);
console.log("删除用户成功");
},
};
// 2. 消息操作示例
export const messageExamples = {
// 创建消息
async createMessage() {
const messageId = await messageService.create({
userId: 1,
content: "这是一条测试消息",
type: "text",
isRead: false,
});
console.log("创建消息成功ID:", messageId);
return messageId;
},
// 查询未读消息
async findUnreadMessages() {
const unreadMessages = await messageService.findWhere("isRead", false);
console.log("未读消息:", unreadMessages);
return unreadMessages;
},
// 标记消息为已读
async markMessageAsRead() {
const count = await messageService.update(1, { isRead: true });
console.log("标记已读消息数量:", count);
return count;
},
// 获取最近消息
async getRecentMessages() {
const recentMessages = await messageService.findAllSorted(
"createdAt",
"desc",
);
console.log("最近消息:", recentMessages.slice(0, 10));
return recentMessages.slice(0, 10);
},
};
// 3. 设置操作示例
export const settingExamples = {
// 保存设置
async saveSetting() {
const settingId = await settingService.create({
key: "theme",
value: "dark",
category: "appearance",
});
console.log("保存设置成功ID:", settingId);
return settingId;
},
// 批量保存设置
async saveMultipleSettings() {
const settingIds = await settingService.createMany([
{ key: "language", value: "zh-CN", category: "general" },
{ key: "fontSize", value: 14, category: "appearance" },
{ key: "autoSave", value: true, category: "editor" },
]);
console.log("批量保存设置成功IDs:", settingIds);
return settingIds;
},
// 查询设置
async getSettings() {
// 查询所有设置
const allSettings = await settingService.findAll();
console.log("所有设置:", allSettings);
// 按分类查询
const appearanceSettings = await settingService.findWhere(
"category",
"appearance",
);
console.log("外观设置:", appearanceSettings);
return { allSettings, appearanceSettings };
},
};
// ============= 业务方法示例 =============
// 4. 用户业务操作示例
export const userBusinessExamples = {
// 根据邮箱查找用户
async findUserByEmail() {
const user = await userBusinessService.findByEmail("zhangsan@example.com");
console.log("根据邮箱查找的用户:", user);
return user;
},
// 查找活跃用户
async findActiveUsers() {
const activeUsers = await userBusinessService.findActiveUsers();
console.log("活跃用户列表:", activeUsers);
return activeUsers;
},
// 搜索用户
async searchUsers() {
const users = await userBusinessService.searchByName("张");
console.log("搜索结果:", users);
return users;
},
// 更新用户状态
async updateUserStatus() {
const count = await userBusinessService.updateStatus(1, "active");
console.log("更新用户状态数量:", count);
return count;
},
};
// 5. 消息业务操作示例
export const messageBusinessExamples = {
// 查找用户消息
async findUserMessages() {
const messages = await messageBusinessService.findByUserId(1);
console.log("用户消息:", messages);
return messages;
},
// 查找未读消息
async findUnreadMessages() {
const unreadMessages = await messageBusinessService.findUnreadMessages();
console.log("未读消息:", unreadMessages);
return unreadMessages;
},
// 标记消息为已读
async markAsRead() {
const count = await messageBusinessService.markAsRead(1);
console.log("标记已读数量:", count);
return count;
},
// 标记用户所有消息为已读
async markAllAsRead() {
const count = await messageBusinessService.markAllAsRead(1);
console.log("标记用户所有消息已读数量:", count);
return count;
},
// 获取最近消息
async getRecentMessages() {
const messages = await messageBusinessService.getRecentMessages(20);
console.log("最近20条消息:", messages);
return messages;
},
};
// 6. 设置业务操作示例
export const settingBusinessExamples = {
// 获取设置值
async getSetting() {
const theme = await settingBusinessService.getSetting("theme");
console.log("主题设置:", theme);
return theme;
},
// 设置值
async setSetting() {
const settingId = await settingBusinessService.setSetting(
"theme",
"light",
"appearance",
);
console.log("设置主题成功ID:", settingId);
return settingId;
},
// 获取分类设置
async getSettingsByCategory() {
const settings =
await settingBusinessService.getSettingsByCategory("appearance");
console.log("外观设置:", settings);
return settings;
},
// 删除设置
async deleteSetting() {
await settingBusinessService.deleteSetting("oldSetting");
console.log("删除设置成功");
},
};
// ============= 数据库工具示例 =============
// 7. 数据库工具操作示例
export const databaseUtilsExamples = {
// 获取数据库统计信息
async getStats() {
const stats = await DatabaseUtils.getStats();
console.log("数据库统计:", stats);
return stats;
},
// 健康检查
async healthCheck() {
const health = await DatabaseUtils.healthCheck();
console.log("数据库健康状态:", health);
return health;
},
// 导出数据
async exportData() {
const jsonData = await DatabaseUtils.exportData();
console.log("导出的数据长度:", jsonData.length);
return jsonData;
},
// 备份到文件
async backupToFile() {
await DatabaseUtils.backupToFile();
console.log("备份文件下载成功");
},
// 从文件恢复(需要文件输入)
async restoreFromFile(file: File) {
try {
await DatabaseUtils.restoreFromFile(file);
console.log("数据恢复成功");
} catch (error) {
console.error("数据恢复失败:", error);
}
},
// 清空所有数据
async clearAllData() {
const confirmed = confirm("确定要清空所有数据吗?此操作不可恢复!");
if (confirmed) {
await DatabaseUtils.clearAllData();
console.log("所有数据已清空");
}
},
};
// ============= 高级查询示例 =============
// 8. 高级查询示例
export const advancedQueryExamples = {
// 复杂条件查询
async complexQuery() {
// 查询活跃用户的未读消息
const activeUsers = await db.users
.where("status")
.equals("active")
.toArray();
const activeUserIds = activeUsers.map(user => user.id!);
const unreadMessages = await db.messages
.where("userId")
.anyOf(activeUserIds)
.and(msg => !msg.isRead)
.toArray();
console.log("活跃用户的未读消息:", unreadMessages);
return unreadMessages;
},
// 统计查询
async statisticsQuery() {
const stats = {
totalUsers: await db.users.count(),
activeUsers: await db.users.where("status").equals("active").count(),
totalMessages: await db.messages.count(),
unreadMessages: await db.messages.where("isRead").equals(false).count(),
messagesThisWeek: await db.messages
.where("createdAt")
.above(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000))
.count(),
};
console.log("统计信息:", stats);
return stats;
},
// 事务操作
async transactionExample() {
try {
await db.transaction("rw", [db.users, db.messages], async () => {
// 创建用户
const userId = await db.users.add({
name: "事务用户",
email: "transaction@example.com",
status: "active",
});
// 为该用户创建欢迎消息
await db.messages.add({
userId,
content: "欢迎使用我们的应用!",
type: "text",
isRead: false,
});
console.log("事务执行成功");
});
} catch (error) {
console.error("事务执行失败:", error);
}
},
};
// ============= 完整使用流程示例 =============
// 9. 完整的应用场景示例
export const fullScenarioExample = {
// 模拟用户注册和使用流程
async simulateUserFlow() {
console.log("=== 开始模拟用户流程 ===");
try {
// 1. 用户注册
const userId = await userBusinessService.create({
name: "新用户",
email: "newuser@example.com",
status: "active",
});
console.log("1. 用户注册成功ID:", userId);
// 2. 设置用户偏好
await settingBusinessService.setSetting(
"theme",
"dark",
"user-" + userId,
);
await settingBusinessService.setSetting(
"language",
"zh-CN",
"user-" + userId,
);
console.log("2. 用户偏好设置完成");
// 3. 发送欢迎消息
const messageId = await messageBusinessService.create({
userId,
content: "欢迎加入我们的平台!",
type: "text",
isRead: false,
});
console.log("3. 欢迎消息发送成功ID:", messageId);
// 4. 用户查看消息
const userMessages = await messageBusinessService.findByUserId(userId);
console.log("4. 用户消息列表:", userMessages);
// 5. 标记消息为已读
await messageBusinessService.markAsRead(messageId);
console.log("5. 消息已标记为已读");
// 6. 获取用户统计信息
const userStats = {
totalMessages: await messageService.countWhere("userId", userId),
unreadMessages: await db.messages
.where("userId")
.equals(userId)
.and(msg => !msg.isRead)
.count(),
};
console.log("6. 用户统计信息:", userStats);
console.log("=== 用户流程模拟完成 ===");
return { userId, messageId, userStats };
} catch (error) {
console.error("用户流程模拟失败:", error);
throw error;
}
},
};
// 导出所有示例
export const allExamples = {
userExamples,
messageExamples,
settingExamples,
userBusinessExamples,
messageBusinessExamples,
settingBusinessExamples,
databaseUtilsExamples,
advancedQueryExamples,
fullScenarioExample,
};
// 快速测试函数
export async function quickTest() {
console.log("=== 开始快速测试 ===");
try {
// 健康检查
const health = await DatabaseUtils.healthCheck();
console.log("数据库健康状态:", health);
// 创建测试数据
const userId = await userBusinessService.create({
name: "测试用户",
email: "test@example.com",
status: "active",
});
const messageId = await messageBusinessService.create({
userId,
content: "测试消息",
type: "text",
isRead: false,
});
// 查询测试
const user = await userBusinessService.findById(userId);
const message = await messageBusinessService.findById(messageId);
console.log("创建的用户:", user);
console.log("创建的消息:", message);
// 统计信息
const stats = await DatabaseUtils.getStats();
console.log("数据库统计:", stats);
console.log("=== 快速测试完成 ===");
return { userId, messageId, stats };
} catch (error) {
console.error("快速测试失败:", error);
throw error;
}
}

446
Cunkebao/src/utils/db.ts Normal file
View File

@@ -0,0 +1,446 @@
import Dexie, { Table } from "dexie";
import { KfUserListData, GroupData, ContractData } from "@/pages/pc/ckbox/data";
// 定义数据库表结构接口
export interface BaseEntity {
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;
}
// 数据库类
class AppDatabase extends Dexie {
users!: Table<User>;
messages!: Table<Message>;
chatRooms!: Table<ChatRoom>;
settings!: Table<Setting>;
constructor() {
super("CunkebaoDatabase");
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",
});
// 自动添加时间戳
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();
});
}
}
// 创建数据库实例
export const db = new AppDatabase();
// 通用数据库操作类
export class DatabaseService<T extends BaseEntity> {
constructor(private table: Table<T>) {}
// 基础 CRUD 操作
async create(
data: Omit<T, "id" | "createdAt" | "updatedAt">,
): 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 });
}
async findById(id: number): Promise<T | undefined> {
return await this.table.get(id);
}
async findAll(): Promise<T[]> {
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 updateMany(
updates: { id: number; data: Partial<Omit<T, "id" | "createdAt">> }[],
): Promise<number> {
return await this.table.bulkUpdate(
updates.map(u => ({ key: u.id, changes: u.data })),
);
}
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> {
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
.where(field as string)
.equals(value)
.toArray();
}
async findWhereIn(field: keyof T, values: any[]): Promise<T[]> {
return await this.table
.where(field as string)
.anyOf(values)
.toArray();
}
async findWhereNot(field: keyof T, value: any): Promise<T[]> {
return await this.table
.where(field as string)
.notEqual(value)
.toArray();
}
// 排序查询
async findAllSorted(
field: keyof T,
direction: "asc" | "desc" = "asc",
): Promise<T[]> {
const collection = this.table.orderBy(field as string);
return direction === "desc"
? await collection.reverse().toArray()
: 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 default db;