collabdocchat 1.2.13 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +219 -218
- package/index.html +2 -0
- package/install-and-start.bat +5 -0
- package/install-and-start.sh +5 -0
- package/package.json +9 -2
- package/scripts/generate-docs.js +448 -0
- package/scripts/pre-publish-check.js +213 -0
- package/scripts/start-app.js +15 -15
- package/server/index.js +38 -6
- package/server/middleware/cache.js +115 -0
- package/server/middleware/errorHandler.js +209 -0
- package/server/models/Document.js +66 -59
- package/server/models/File.js +49 -43
- package/server/models/Group.js +6 -0
- package/server/models/KnowledgeBase.js +254 -0
- package/server/models/Message.js +43 -0
- package/server/models/Task.js +87 -55
- package/server/models/User.js +67 -60
- package/server/models/Workflow.js +249 -0
- package/server/routes/ai.js +327 -0
- package/server/routes/audit.js +245 -210
- package/server/routes/backup.js +108 -0
- package/server/routes/chunked-upload.js +343 -0
- package/server/routes/export.js +440 -0
- package/server/routes/files.js +294 -218
- package/server/routes/groups.js +182 -0
- package/server/routes/knowledge.js +509 -0
- package/server/routes/tasks.js +257 -110
- package/server/routes/workflows.js +380 -0
- package/server/utils/backup.js +439 -0
- package/server/utils/cache.js +223 -0
- package/server/utils/workflow-engine.js +479 -0
- package/server/websocket/enhanced.js +509 -0
- package/server/websocket/index.js +233 -1
- package/src/components/knowledge-modal.js +485 -0
- package/src/components/optimized-poll-detail.js +724 -0
- package/src/main.js +5 -0
- package/src/pages/admin-dashboard.js +2248 -44
- package/src/pages/optimized-backup-view.js +616 -0
- package/src/pages/optimized-knowledge-view.js +803 -0
- package/src/pages/optimized-task-detail.js +843 -0
- package/src/pages/optimized-workflow-view.js +806 -0
- package/src/pages/simplified-workflows.js +651 -0
- package/src/pages/user-dashboard.js +677 -58
- package/src/services/api.js +64 -0
- package/src/services/auth.js +1 -1
- package/src/services/websocket.js +124 -16
- package/src/styles/collaboration-modern.js +708 -0
- package/src/styles/enhancements.css +392 -0
- package/src/styles/main.css +620 -1420
- package/src/styles/responsive.css +1000 -0
- package/src/styles/sidebar-fix.css +60 -0
- package/src/utils/ai-assistant.js +1398 -0
- package/src/utils/chat-enhancements.js +509 -0
- package/src/utils/collaboration-enhancer.js +1151 -0
- package/src/utils/feature-integrator.js +1724 -0
- package/src/utils/onboarding-guide.js +734 -0
- package/src/utils/performance.js +394 -0
- package/src/utils/permission-manager.js +890 -0
- package/src/utils/responsive-handler.js +491 -0
- package/src/utils/theme-manager.js +811 -0
- package/src/utils/ui-enhancements-loader.js +329 -0
- package/USAGE.md +0 -298
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname } from 'path';
|
|
5
|
+
import archiver from 'archiver';
|
|
6
|
+
import mongoose from 'mongoose';
|
|
7
|
+
import cron from 'node-cron';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
|
|
12
|
+
class BackupService {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.backupDir = path.join(__dirname, '../../backups');
|
|
15
|
+
this.maxBackups = 30; // 保留最近30个备份
|
|
16
|
+
this.isBackingUp = false;
|
|
17
|
+
|
|
18
|
+
// 确保备份目录存在
|
|
19
|
+
if (!fs.existsSync(this.backupDir)) {
|
|
20
|
+
fs.mkdirSync(this.backupDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 初始化自动备份任务
|
|
25
|
+
initAutoBackup() {
|
|
26
|
+
// 每天凌晨2点执行备份
|
|
27
|
+
cron.schedule('0 2 * * *', async () => {
|
|
28
|
+
console.log('开始执行自动备份...');
|
|
29
|
+
try {
|
|
30
|
+
await this.createBackup('auto');
|
|
31
|
+
console.log('自动备份完成');
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('自动备份失败:', error);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
console.log('✅ 自动备份任务已启动(每天凌晨2点执行)');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 创建备份
|
|
41
|
+
async createBackup(type = 'manual', description = '') {
|
|
42
|
+
if (this.isBackingUp) {
|
|
43
|
+
throw new Error('备份正在进行中,请稍后再试');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.isBackingUp = true;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
50
|
+
const backupName = `backup_${type}_${timestamp}`;
|
|
51
|
+
const backupPath = path.join(this.backupDir, backupName);
|
|
52
|
+
|
|
53
|
+
// 创建备份目录
|
|
54
|
+
if (!fs.existsSync(backupPath)) {
|
|
55
|
+
fs.mkdirSync(backupPath, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(`创建备份: ${backupName}`);
|
|
59
|
+
|
|
60
|
+
// 1. 导出数据库
|
|
61
|
+
await this.exportDatabase(backupPath);
|
|
62
|
+
|
|
63
|
+
// 2. 备份上传文件
|
|
64
|
+
await this.backupUploads(backupPath);
|
|
65
|
+
|
|
66
|
+
// 3. 创建备份元数据
|
|
67
|
+
const metadata = {
|
|
68
|
+
name: backupName,
|
|
69
|
+
type,
|
|
70
|
+
description,
|
|
71
|
+
createdAt: new Date().toISOString(),
|
|
72
|
+
size: this.getDirectorySize(backupPath),
|
|
73
|
+
collections: await this.getCollectionStats()
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
fs.writeFileSync(
|
|
77
|
+
path.join(backupPath, 'metadata.json'),
|
|
78
|
+
JSON.stringify(metadata, null, 2)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// 4. 压缩备份
|
|
82
|
+
const zipPath = await this.compressBackup(backupPath, backupName);
|
|
83
|
+
|
|
84
|
+
// 5. 删除未压缩的备份目录
|
|
85
|
+
this.removeDirectory(backupPath);
|
|
86
|
+
|
|
87
|
+
// 6. 清理旧备份
|
|
88
|
+
await this.cleanOldBackups();
|
|
89
|
+
|
|
90
|
+
console.log(`备份完成: ${zipPath}`);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
success: true,
|
|
94
|
+
backupName,
|
|
95
|
+
zipPath,
|
|
96
|
+
metadata
|
|
97
|
+
};
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('备份失败:', error);
|
|
100
|
+
throw error;
|
|
101
|
+
} finally {
|
|
102
|
+
this.isBackingUp = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 导出数据库
|
|
107
|
+
async exportDatabase(backupPath) {
|
|
108
|
+
const dbPath = path.join(backupPath, 'database');
|
|
109
|
+
if (!fs.existsSync(dbPath)) {
|
|
110
|
+
fs.mkdirSync(dbPath, { recursive: true });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 获取所有集合
|
|
114
|
+
const collections = await mongoose.connection.db.listCollections().toArray();
|
|
115
|
+
|
|
116
|
+
for (const collection of collections) {
|
|
117
|
+
const collectionName = collection.name;
|
|
118
|
+
console.log(`导出集合: ${collectionName}`);
|
|
119
|
+
|
|
120
|
+
const data = await mongoose.connection.db
|
|
121
|
+
.collection(collectionName)
|
|
122
|
+
.find({})
|
|
123
|
+
.toArray();
|
|
124
|
+
|
|
125
|
+
fs.writeFileSync(
|
|
126
|
+
path.join(dbPath, `${collectionName}.json`),
|
|
127
|
+
JSON.stringify(data, null, 2)
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 备份上传文件
|
|
133
|
+
async backupUploads(backupPath) {
|
|
134
|
+
const uploadsDir = path.join(__dirname, '../../uploads');
|
|
135
|
+
const backupUploadsDir = path.join(backupPath, 'uploads');
|
|
136
|
+
|
|
137
|
+
if (!fs.existsSync(uploadsDir)) {
|
|
138
|
+
console.log('上传目录不存在,跳过文件备份');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log('备份上传文件...');
|
|
143
|
+
this.copyDirectory(uploadsDir, backupUploadsDir);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 压缩备份
|
|
147
|
+
async compressBackup(backupPath, backupName) {
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
const zipPath = `${backupPath}.zip`;
|
|
150
|
+
const output = fs.createWriteStream(zipPath);
|
|
151
|
+
const archive = archiver('zip', {
|
|
152
|
+
zlib: { level: 9 }
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
output.on('close', () => {
|
|
156
|
+
console.log(`备份已压缩: ${archive.pointer()} bytes`);
|
|
157
|
+
resolve(zipPath);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
archive.on('error', (err) => {
|
|
161
|
+
reject(err);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
archive.pipe(output);
|
|
165
|
+
archive.directory(backupPath, false);
|
|
166
|
+
archive.finalize();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 恢复备份
|
|
171
|
+
async restoreBackup(backupName) {
|
|
172
|
+
const backupZipPath = path.join(this.backupDir, `${backupName}.zip`);
|
|
173
|
+
|
|
174
|
+
if (!fs.existsSync(backupZipPath)) {
|
|
175
|
+
throw new Error('备份文件不存在');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(`开始恢复备份: ${backupName}`);
|
|
179
|
+
|
|
180
|
+
// 1. 解压备份
|
|
181
|
+
const extractPath = path.join(this.backupDir, 'temp_restore');
|
|
182
|
+
if (fs.existsSync(extractPath)) {
|
|
183
|
+
this.removeDirectory(extractPath);
|
|
184
|
+
}
|
|
185
|
+
fs.mkdirSync(extractPath, { recursive: true });
|
|
186
|
+
|
|
187
|
+
await this.extractZip(backupZipPath, extractPath);
|
|
188
|
+
|
|
189
|
+
// 2. 读取元数据
|
|
190
|
+
const metadata = JSON.parse(
|
|
191
|
+
fs.readFileSync(path.join(extractPath, 'metadata.json'), 'utf8')
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
console.log('备份元数据:', metadata);
|
|
195
|
+
|
|
196
|
+
// 3. 恢复数据库
|
|
197
|
+
await this.restoreDatabase(path.join(extractPath, 'database'));
|
|
198
|
+
|
|
199
|
+
// 4. 恢复上传文件
|
|
200
|
+
await this.restoreUploads(path.join(extractPath, 'uploads'));
|
|
201
|
+
|
|
202
|
+
// 5. 清理临时文件
|
|
203
|
+
this.removeDirectory(extractPath);
|
|
204
|
+
|
|
205
|
+
console.log('备份恢复完成');
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
success: true,
|
|
209
|
+
metadata
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 恢复数据库
|
|
214
|
+
async restoreDatabase(dbPath) {
|
|
215
|
+
if (!fs.existsSync(dbPath)) {
|
|
216
|
+
console.log('数据库备份不存在,跳过恢复');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const files = fs.readdirSync(dbPath);
|
|
221
|
+
|
|
222
|
+
for (const file of files) {
|
|
223
|
+
if (!file.endsWith('.json')) continue;
|
|
224
|
+
|
|
225
|
+
const collectionName = file.replace('.json', '');
|
|
226
|
+
console.log(`恢复集合: ${collectionName}`);
|
|
227
|
+
|
|
228
|
+
const data = JSON.parse(
|
|
229
|
+
fs.readFileSync(path.join(dbPath, file), 'utf8')
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// 清空现有数据
|
|
233
|
+
await mongoose.connection.db.collection(collectionName).deleteMany({});
|
|
234
|
+
|
|
235
|
+
// 插入备份数据
|
|
236
|
+
if (data.length > 0) {
|
|
237
|
+
await mongoose.connection.db.collection(collectionName).insertMany(data);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 恢复上传文件
|
|
243
|
+
async restoreUploads(backupUploadsDir) {
|
|
244
|
+
if (!fs.existsSync(backupUploadsDir)) {
|
|
245
|
+
console.log('上传文件备份不存在,跳过恢复');
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const uploadsDir = path.join(__dirname, '../../uploads');
|
|
250
|
+
|
|
251
|
+
console.log('恢复上传文件...');
|
|
252
|
+
|
|
253
|
+
// 备份当前上传文件
|
|
254
|
+
if (fs.existsSync(uploadsDir)) {
|
|
255
|
+
const backupCurrentUploads = path.join(
|
|
256
|
+
this.backupDir,
|
|
257
|
+
`uploads_before_restore_${Date.now()}`
|
|
258
|
+
);
|
|
259
|
+
this.copyDirectory(uploadsDir, backupCurrentUploads);
|
|
260
|
+
console.log(`当前上传文件已备份到: ${backupCurrentUploads}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 恢复上传文件
|
|
264
|
+
this.copyDirectory(backupUploadsDir, uploadsDir);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 获取备份列表
|
|
268
|
+
getBackupList() {
|
|
269
|
+
const files = fs.readdirSync(this.backupDir);
|
|
270
|
+
const backups = [];
|
|
271
|
+
|
|
272
|
+
for (const file of files) {
|
|
273
|
+
if (!file.endsWith('.zip')) continue;
|
|
274
|
+
|
|
275
|
+
const filePath = path.join(this.backupDir, file);
|
|
276
|
+
const stats = fs.statSync(filePath);
|
|
277
|
+
|
|
278
|
+
backups.push({
|
|
279
|
+
name: file.replace('.zip', ''),
|
|
280
|
+
filename: file,
|
|
281
|
+
size: stats.size,
|
|
282
|
+
createdAt: stats.birthtime,
|
|
283
|
+
path: filePath
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 按创建时间倒序排列
|
|
288
|
+
backups.sort((a, b) => b.createdAt - a.createdAt);
|
|
289
|
+
|
|
290
|
+
return backups;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 删除备份
|
|
294
|
+
deleteBackup(backupName) {
|
|
295
|
+
const backupPath = path.join(this.backupDir, `${backupName}.zip`);
|
|
296
|
+
|
|
297
|
+
if (!fs.existsSync(backupPath)) {
|
|
298
|
+
throw new Error('备份文件不存在');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
fs.unlinkSync(backupPath);
|
|
302
|
+
console.log(`备份已删除: ${backupName}`);
|
|
303
|
+
|
|
304
|
+
return { success: true };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 清理旧备份
|
|
308
|
+
async cleanOldBackups() {
|
|
309
|
+
const backups = this.getBackupList();
|
|
310
|
+
|
|
311
|
+
if (backups.length <= this.maxBackups) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
console.log(`清理旧备份,保留最近 ${this.maxBackups} 个`);
|
|
316
|
+
|
|
317
|
+
const backupsToDelete = backups.slice(this.maxBackups);
|
|
318
|
+
|
|
319
|
+
for (const backup of backupsToDelete) {
|
|
320
|
+
try {
|
|
321
|
+
fs.unlinkSync(backup.path);
|
|
322
|
+
console.log(`已删除旧备份: ${backup.name}`);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error(`删除备份失败: ${backup.name}`, error);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 获取集合统计信息
|
|
330
|
+
async getCollectionStats() {
|
|
331
|
+
const collections = await mongoose.connection.db.listCollections().toArray();
|
|
332
|
+
const stats = {};
|
|
333
|
+
|
|
334
|
+
for (const collection of collections) {
|
|
335
|
+
const count = await mongoose.connection.db
|
|
336
|
+
.collection(collection.name)
|
|
337
|
+
.countDocuments();
|
|
338
|
+
stats[collection.name] = count;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return stats;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 获取目录大小
|
|
345
|
+
getDirectorySize(dirPath) {
|
|
346
|
+
let size = 0;
|
|
347
|
+
|
|
348
|
+
if (!fs.existsSync(dirPath)) {
|
|
349
|
+
return size;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const files = fs.readdirSync(dirPath);
|
|
353
|
+
|
|
354
|
+
for (const file of files) {
|
|
355
|
+
const filePath = path.join(dirPath, file);
|
|
356
|
+
const stats = fs.statSync(filePath);
|
|
357
|
+
|
|
358
|
+
if (stats.isDirectory()) {
|
|
359
|
+
size += this.getDirectorySize(filePath);
|
|
360
|
+
} else {
|
|
361
|
+
size += stats.size;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return size;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 复制目录
|
|
369
|
+
copyDirectory(src, dest) {
|
|
370
|
+
if (!fs.existsSync(dest)) {
|
|
371
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const files = fs.readdirSync(src);
|
|
375
|
+
|
|
376
|
+
for (const file of files) {
|
|
377
|
+
const srcPath = path.join(src, file);
|
|
378
|
+
const destPath = path.join(dest, file);
|
|
379
|
+
const stats = fs.statSync(srcPath);
|
|
380
|
+
|
|
381
|
+
if (stats.isDirectory()) {
|
|
382
|
+
this.copyDirectory(srcPath, destPath);
|
|
383
|
+
} else {
|
|
384
|
+
fs.copyFileSync(srcPath, destPath);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// 删除目录
|
|
390
|
+
removeDirectory(dirPath) {
|
|
391
|
+
if (!fs.existsSync(dirPath)) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const files = fs.readdirSync(dirPath);
|
|
396
|
+
|
|
397
|
+
for (const file of files) {
|
|
398
|
+
const filePath = path.join(dirPath, file);
|
|
399
|
+
const stats = fs.statSync(filePath);
|
|
400
|
+
|
|
401
|
+
if (stats.isDirectory()) {
|
|
402
|
+
this.removeDirectory(filePath);
|
|
403
|
+
} else {
|
|
404
|
+
fs.unlinkSync(filePath);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
fs.rmdirSync(dirPath);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// 解压ZIP文件
|
|
412
|
+
async extractZip(zipPath, extractPath) {
|
|
413
|
+
// 注意:这里需要使用 unzipper 或 extract-zip 库
|
|
414
|
+
// 为了简化,这里使用命令行工具
|
|
415
|
+
const { exec } = await import('child_process');
|
|
416
|
+
const { promisify } = await import('util');
|
|
417
|
+
const execAsync = promisify(exec);
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
// Windows
|
|
421
|
+
await execAsync(`powershell Expand-Archive -Path "${zipPath}" -DestinationPath "${extractPath}" -Force`);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
// Linux/Mac
|
|
424
|
+
await execAsync(`unzip -o "${zipPath}" -d "${extractPath}"`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 格式化文件大小
|
|
429
|
+
formatSize(bytes) {
|
|
430
|
+
if (bytes === 0) return '0 Bytes';
|
|
431
|
+
const k = 1024;
|
|
432
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
433
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
434
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export default new BackupService();
|
|
439
|
+
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { createClient } from 'redis';
|
|
2
|
+
|
|
3
|
+
class CacheService {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.client = null;
|
|
6
|
+
this.isConnected = false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async connect() {
|
|
10
|
+
try {
|
|
11
|
+
this.client = createClient({
|
|
12
|
+
url: process.env.REDIS_URL || 'redis://localhost:6379',
|
|
13
|
+
socket: {
|
|
14
|
+
reconnectStrategy: (retries) => {
|
|
15
|
+
if (retries > 10) {
|
|
16
|
+
console.log('❌ Redis 重连次数过多,停止重连');
|
|
17
|
+
return new Error('Redis 连接失败');
|
|
18
|
+
}
|
|
19
|
+
return retries * 100;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
this.client.on('error', (err) => {
|
|
25
|
+
console.error('❌ Redis 错误:', err);
|
|
26
|
+
this.isConnected = false;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
this.client.on('connect', () => {
|
|
30
|
+
console.log('🔌 Redis 正在连接...');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
this.client.on('ready', () => {
|
|
34
|
+
console.log('✅ Redis 连接成功');
|
|
35
|
+
this.isConnected = true;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
this.client.on('reconnecting', () => {
|
|
39
|
+
console.log('🔄 Redis 正在重连...');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await this.client.connect();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('❌ Redis 连接失败:', error.message);
|
|
45
|
+
console.log('⚠️ 应用将在没有缓存的情况下运行');
|
|
46
|
+
this.isConnected = false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async get(key) {
|
|
51
|
+
if (!this.isConnected) return null;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const data = await this.client.get(key);
|
|
55
|
+
return data ? JSON.parse(data) : null;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('Redis GET 错误:', error);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async set(key, value, ttl = 300) {
|
|
63
|
+
if (!this.isConnected) return false;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await this.client.setEx(key, ttl, JSON.stringify(value));
|
|
67
|
+
return true;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('Redis SET 错误:', error);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async del(key) {
|
|
75
|
+
if (!this.isConnected) return false;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await this.client.del(key);
|
|
79
|
+
return true;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Redis DEL 错误:', error);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async delPattern(pattern) {
|
|
87
|
+
if (!this.isConnected) return false;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const keys = await this.client.keys(pattern);
|
|
91
|
+
if (keys.length > 0) {
|
|
92
|
+
await this.client.del(keys);
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('Redis DEL PATTERN 错误:', error);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async exists(key) {
|
|
102
|
+
if (!this.isConnected) return false;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
return await this.client.exists(key);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('Redis EXISTS 错误:', error);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async increment(key, ttl = 3600) {
|
|
113
|
+
if (!this.isConnected) return 0;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const value = await this.client.incr(key);
|
|
117
|
+
if (value === 1) {
|
|
118
|
+
await this.client.expire(key, ttl);
|
|
119
|
+
}
|
|
120
|
+
return value;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('Redis INCR 错误:', error);
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 缓存用户信息
|
|
128
|
+
async cacheUser(userId, userData, ttl = 600) {
|
|
129
|
+
return await this.set(`user:${userId}`, userData, ttl);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async getCachedUser(userId) {
|
|
133
|
+
return await this.get(`user:${userId}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async invalidateUser(userId) {
|
|
137
|
+
return await this.del(`user:${userId}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 缓存群组列表
|
|
141
|
+
async cacheGroupList(userId, groups, ttl = 300) {
|
|
142
|
+
return await this.set(`groups:user:${userId}`, groups, ttl);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async getCachedGroupList(userId) {
|
|
146
|
+
return await this.get(`groups:user:${userId}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async invalidateGroupList(userId) {
|
|
150
|
+
return await this.del(`groups:user:${userId}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 缓存群组详情
|
|
154
|
+
async cacheGroup(groupId, groupData, ttl = 300) {
|
|
155
|
+
return await this.set(`group:${groupId}`, groupData, ttl);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async getCachedGroup(groupId) {
|
|
159
|
+
return await this.get(`group:${groupId}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async invalidateGroup(groupId) {
|
|
163
|
+
await this.del(`group:${groupId}`);
|
|
164
|
+
// 同时清除相关的群组列表缓存
|
|
165
|
+
await this.delPattern(`groups:user:*`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 缓存文档列表
|
|
169
|
+
async cacheDocumentList(groupId, documents, ttl = 300) {
|
|
170
|
+
return await this.set(`documents:group:${groupId}`, documents, ttl);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async getCachedDocumentList(groupId) {
|
|
174
|
+
return await this.get(`documents:group:${groupId}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async invalidateDocumentList(groupId) {
|
|
178
|
+
return await this.del(`documents:group:${groupId}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 缓存任务列表
|
|
182
|
+
async cacheTaskList(groupId, tasks, ttl = 300) {
|
|
183
|
+
return await this.set(`tasks:group:${groupId}`, tasks, ttl);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async getCachedTaskList(groupId) {
|
|
187
|
+
return await this.get(`tasks:group:${groupId}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async invalidateTaskList(groupId) {
|
|
191
|
+
return await this.del(`tasks:group:${groupId}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 缓存用户任务
|
|
195
|
+
async cacheUserTasks(userId, tasks, ttl = 300) {
|
|
196
|
+
return await this.set(`tasks:user:${userId}`, tasks, ttl);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async getCachedUserTasks(userId) {
|
|
200
|
+
return await this.get(`tasks:user:${userId}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async invalidateUserTasks(userId) {
|
|
204
|
+
return await this.del(`tasks:user:${userId}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 速率限制
|
|
208
|
+
async checkRateLimit(key, limit = 100, window = 60) {
|
|
209
|
+
const count = await this.increment(`ratelimit:${key}`, window);
|
|
210
|
+
return count <= limit;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async disconnect() {
|
|
214
|
+
if (this.client && this.isConnected) {
|
|
215
|
+
await this.client.quit();
|
|
216
|
+
console.log('👋 Redis 连接已关闭');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export default new CacheService();
|
|
222
|
+
|
|
223
|
+
|