collabdocchat 1.0.0
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/LICENSE +21 -0
- package/README.md +501 -0
- package/package.json +70 -0
- package/server/index.js +63 -0
- package/server/middleware/auth.js +26 -0
- package/server/models/AuditLog.js +90 -0
- package/server/models/Document.js +59 -0
- package/server/models/File.js +43 -0
- package/server/models/Group.js +61 -0
- package/server/models/Message.js +31 -0
- package/server/models/Task.js +55 -0
- package/server/models/User.js +60 -0
- package/server/routes/audit.js +210 -0
- package/server/routes/auth.js +125 -0
- package/server/routes/documents.js +254 -0
- package/server/routes/files.js +218 -0
- package/server/routes/groups.js +317 -0
- package/server/routes/tasks.js +110 -0
- package/server/utils/auditLogger.js +238 -0
- package/server/utils/initAdmin.js +51 -0
- package/server/websocket/index.js +228 -0
- package/src/main.js +53 -0
- package/src/pages/admin-dashboard.js +1493 -0
- package/src/pages/login.js +101 -0
- package/src/pages/user-dashboard.js +906 -0
- package/src/services/api.js +265 -0
- package/src/services/auth.js +54 -0
- package/src/services/websocket.js +80 -0
- package/src/styles/main.css +1421 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
|
|
3
|
+
const auditLogSchema = new mongoose.Schema({
|
|
4
|
+
action: {
|
|
5
|
+
type: String,
|
|
6
|
+
required: true,
|
|
7
|
+
enum: [
|
|
8
|
+
'content_edit',
|
|
9
|
+
'title_edit',
|
|
10
|
+
'document_create',
|
|
11
|
+
'document_update',
|
|
12
|
+
'document_delete',
|
|
13
|
+
'document_share',
|
|
14
|
+
'user_login',
|
|
15
|
+
'user_logout',
|
|
16
|
+
'user_register',
|
|
17
|
+
'group_create',
|
|
18
|
+
'group_update',
|
|
19
|
+
'group_delete',
|
|
20
|
+
'member_add',
|
|
21
|
+
'member_remove',
|
|
22
|
+
'permission_change',
|
|
23
|
+
'task_create',
|
|
24
|
+
'task_update',
|
|
25
|
+
'task_delete',
|
|
26
|
+
'comment_create',
|
|
27
|
+
'comment_update',
|
|
28
|
+
'comment_delete'
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
user: {
|
|
32
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
33
|
+
ref: 'User',
|
|
34
|
+
required: true
|
|
35
|
+
},
|
|
36
|
+
resourceType: {
|
|
37
|
+
type: String,
|
|
38
|
+
required: true,
|
|
39
|
+
enum: ['document', 'group', 'user', 'task', 'comment', 'message']
|
|
40
|
+
},
|
|
41
|
+
resourceId: {
|
|
42
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
43
|
+
required: true
|
|
44
|
+
},
|
|
45
|
+
resourceTitle: {
|
|
46
|
+
type: String,
|
|
47
|
+
default: ''
|
|
48
|
+
},
|
|
49
|
+
details: {
|
|
50
|
+
oldValue: mongoose.Schema.Types.Mixed,
|
|
51
|
+
newValue: mongoose.Schema.Types.Mixed,
|
|
52
|
+
field: String,
|
|
53
|
+
description: String
|
|
54
|
+
},
|
|
55
|
+
changes: {
|
|
56
|
+
insertions: [{
|
|
57
|
+
position: Number,
|
|
58
|
+
content: String,
|
|
59
|
+
length: Number
|
|
60
|
+
}],
|
|
61
|
+
deletions: [{
|
|
62
|
+
position: Number,
|
|
63
|
+
content: String,
|
|
64
|
+
length: Number
|
|
65
|
+
}]
|
|
66
|
+
},
|
|
67
|
+
metadata: {
|
|
68
|
+
groupId: {
|
|
69
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
70
|
+
ref: 'Group'
|
|
71
|
+
},
|
|
72
|
+
ipAddress: String,
|
|
73
|
+
userAgent: String
|
|
74
|
+
}
|
|
75
|
+
}, {
|
|
76
|
+
timestamps: true
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// 索引优化查询性能
|
|
80
|
+
auditLogSchema.index({ user: 1, createdAt: -1 });
|
|
81
|
+
auditLogSchema.index({ resourceId: 1, resourceType: 1, createdAt: -1 });
|
|
82
|
+
auditLogSchema.index({ action: 1, createdAt: -1 });
|
|
83
|
+
auditLogSchema.index({ 'metadata.groupId': 1, createdAt: -1 });
|
|
84
|
+
|
|
85
|
+
export default mongoose.model('AuditLog', auditLogSchema);
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
|
|
3
|
+
const documentSchema = new mongoose.Schema({
|
|
4
|
+
title: {
|
|
5
|
+
type: String,
|
|
6
|
+
required: true,
|
|
7
|
+
trim: true
|
|
8
|
+
},
|
|
9
|
+
content: {
|
|
10
|
+
type: String,
|
|
11
|
+
default: ''
|
|
12
|
+
},
|
|
13
|
+
group: {
|
|
14
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
15
|
+
ref: 'Group',
|
|
16
|
+
required: true
|
|
17
|
+
},
|
|
18
|
+
creator: {
|
|
19
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
20
|
+
ref: 'User',
|
|
21
|
+
required: true
|
|
22
|
+
},
|
|
23
|
+
permission: {
|
|
24
|
+
type: String,
|
|
25
|
+
enum: ['readonly', 'editable'],
|
|
26
|
+
default: 'editable'
|
|
27
|
+
},
|
|
28
|
+
editors: [{
|
|
29
|
+
user: {
|
|
30
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
31
|
+
ref: 'User'
|
|
32
|
+
},
|
|
33
|
+
lastEdit: {
|
|
34
|
+
type: Date,
|
|
35
|
+
default: Date.now
|
|
36
|
+
}
|
|
37
|
+
}],
|
|
38
|
+
versions: [{
|
|
39
|
+
content: String,
|
|
40
|
+
editor: {
|
|
41
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
42
|
+
ref: 'User'
|
|
43
|
+
},
|
|
44
|
+
timestamp: {
|
|
45
|
+
type: Date,
|
|
46
|
+
default: Date.now
|
|
47
|
+
}
|
|
48
|
+
}],
|
|
49
|
+
yjsState: {
|
|
50
|
+
type: Buffer,
|
|
51
|
+
default: null
|
|
52
|
+
}
|
|
53
|
+
}, {
|
|
54
|
+
timestamps: true
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export default mongoose.model('Document', documentSchema);
|
|
58
|
+
|
|
59
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
|
|
3
|
+
const fileSchema = new mongoose.Schema({
|
|
4
|
+
filename: {
|
|
5
|
+
type: String,
|
|
6
|
+
required: true
|
|
7
|
+
},
|
|
8
|
+
originalName: {
|
|
9
|
+
type: String,
|
|
10
|
+
required: true
|
|
11
|
+
},
|
|
12
|
+
path: {
|
|
13
|
+
type: String,
|
|
14
|
+
required: true
|
|
15
|
+
},
|
|
16
|
+
mimetype: {
|
|
17
|
+
type: String,
|
|
18
|
+
required: true
|
|
19
|
+
},
|
|
20
|
+
size: {
|
|
21
|
+
type: Number,
|
|
22
|
+
required: true
|
|
23
|
+
},
|
|
24
|
+
group: {
|
|
25
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
26
|
+
ref: 'Group',
|
|
27
|
+
required: true
|
|
28
|
+
},
|
|
29
|
+
uploader: {
|
|
30
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
31
|
+
ref: 'User',
|
|
32
|
+
required: true
|
|
33
|
+
},
|
|
34
|
+
description: {
|
|
35
|
+
type: String,
|
|
36
|
+
default: ''
|
|
37
|
+
}
|
|
38
|
+
}, {
|
|
39
|
+
timestamps: true
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export default mongoose.model('File', fileSchema);
|
|
43
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
|
|
3
|
+
const groupSchema = new mongoose.Schema({
|
|
4
|
+
name: {
|
|
5
|
+
type: String,
|
|
6
|
+
required: true,
|
|
7
|
+
trim: true
|
|
8
|
+
},
|
|
9
|
+
description: {
|
|
10
|
+
type: String,
|
|
11
|
+
default: ''
|
|
12
|
+
},
|
|
13
|
+
admin: {
|
|
14
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
15
|
+
ref: 'User',
|
|
16
|
+
required: true
|
|
17
|
+
},
|
|
18
|
+
members: [{
|
|
19
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
20
|
+
ref: 'User'
|
|
21
|
+
}],
|
|
22
|
+
documents: [{
|
|
23
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
24
|
+
ref: 'Document'
|
|
25
|
+
}],
|
|
26
|
+
files: [{
|
|
27
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
28
|
+
ref: 'File'
|
|
29
|
+
}],
|
|
30
|
+
tasks: [{
|
|
31
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
32
|
+
ref: 'Task'
|
|
33
|
+
}],
|
|
34
|
+
mutedAll: {
|
|
35
|
+
type: Boolean,
|
|
36
|
+
default: false
|
|
37
|
+
},
|
|
38
|
+
mutedUsers: [{
|
|
39
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
40
|
+
ref: 'User'
|
|
41
|
+
}],
|
|
42
|
+
callHistory: [{
|
|
43
|
+
calledUser: {
|
|
44
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
45
|
+
ref: 'User'
|
|
46
|
+
},
|
|
47
|
+
timestamp: {
|
|
48
|
+
type: Date,
|
|
49
|
+
default: Date.now
|
|
50
|
+
},
|
|
51
|
+
responded: {
|
|
52
|
+
type: Boolean,
|
|
53
|
+
default: false
|
|
54
|
+
}
|
|
55
|
+
}]
|
|
56
|
+
}, {
|
|
57
|
+
timestamps: true
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export default mongoose.model('Group', groupSchema);
|
|
61
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
|
|
3
|
+
const messageSchema = new mongoose.Schema({
|
|
4
|
+
group: {
|
|
5
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
6
|
+
ref: 'Group',
|
|
7
|
+
required: true
|
|
8
|
+
},
|
|
9
|
+
sender: {
|
|
10
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
11
|
+
ref: 'User',
|
|
12
|
+
required: true
|
|
13
|
+
},
|
|
14
|
+
username: {
|
|
15
|
+
type: String,
|
|
16
|
+
required: true
|
|
17
|
+
},
|
|
18
|
+
content: {
|
|
19
|
+
type: String,
|
|
20
|
+
required: true
|
|
21
|
+
},
|
|
22
|
+
timestamp: {
|
|
23
|
+
type: Date,
|
|
24
|
+
default: Date.now
|
|
25
|
+
}
|
|
26
|
+
}, {
|
|
27
|
+
timestamps: true
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export default mongoose.model('Message', messageSchema);
|
|
31
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
|
|
3
|
+
const taskSchema = new mongoose.Schema({
|
|
4
|
+
title: {
|
|
5
|
+
type: String,
|
|
6
|
+
required: true,
|
|
7
|
+
trim: true
|
|
8
|
+
},
|
|
9
|
+
description: {
|
|
10
|
+
type: String,
|
|
11
|
+
default: ''
|
|
12
|
+
},
|
|
13
|
+
group: {
|
|
14
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
15
|
+
ref: 'Group',
|
|
16
|
+
required: true
|
|
17
|
+
},
|
|
18
|
+
creator: {
|
|
19
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
20
|
+
ref: 'User',
|
|
21
|
+
required: true
|
|
22
|
+
},
|
|
23
|
+
assignedTo: [{
|
|
24
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
25
|
+
ref: 'User'
|
|
26
|
+
}],
|
|
27
|
+
relatedDocument: {
|
|
28
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
29
|
+
ref: 'Document'
|
|
30
|
+
},
|
|
31
|
+
status: {
|
|
32
|
+
type: String,
|
|
33
|
+
enum: ['pending', 'in_progress', 'completed', 'terminated'],
|
|
34
|
+
default: 'pending'
|
|
35
|
+
},
|
|
36
|
+
deadline: {
|
|
37
|
+
type: Date
|
|
38
|
+
},
|
|
39
|
+
completedBy: [{
|
|
40
|
+
user: {
|
|
41
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
42
|
+
ref: 'User'
|
|
43
|
+
},
|
|
44
|
+
completedAt: {
|
|
45
|
+
type: Date,
|
|
46
|
+
default: Date.now
|
|
47
|
+
}
|
|
48
|
+
}]
|
|
49
|
+
}, {
|
|
50
|
+
timestamps: true
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export default mongoose.model('Task', taskSchema);
|
|
54
|
+
|
|
55
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
import bcrypt from 'bcryptjs';
|
|
3
|
+
|
|
4
|
+
const userSchema = new mongoose.Schema({
|
|
5
|
+
username: {
|
|
6
|
+
type: String,
|
|
7
|
+
required: true,
|
|
8
|
+
unique: true,
|
|
9
|
+
trim: true
|
|
10
|
+
},
|
|
11
|
+
email: {
|
|
12
|
+
type: String,
|
|
13
|
+
required: true,
|
|
14
|
+
unique: true,
|
|
15
|
+
lowercase: true
|
|
16
|
+
},
|
|
17
|
+
password: {
|
|
18
|
+
type: String,
|
|
19
|
+
required: true
|
|
20
|
+
},
|
|
21
|
+
role: {
|
|
22
|
+
type: String,
|
|
23
|
+
enum: ['admin', 'user'],
|
|
24
|
+
default: 'user'
|
|
25
|
+
},
|
|
26
|
+
avatar: {
|
|
27
|
+
type: String,
|
|
28
|
+
default: ''
|
|
29
|
+
},
|
|
30
|
+
groups: [{
|
|
31
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
32
|
+
ref: 'Group'
|
|
33
|
+
}],
|
|
34
|
+
isOnline: {
|
|
35
|
+
type: Boolean,
|
|
36
|
+
default: false
|
|
37
|
+
},
|
|
38
|
+
lastSeen: {
|
|
39
|
+
type: Date,
|
|
40
|
+
default: Date.now
|
|
41
|
+
}
|
|
42
|
+
}, {
|
|
43
|
+
timestamps: true
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// 密码加密
|
|
47
|
+
userSchema.pre('save', async function(next) {
|
|
48
|
+
if (!this.isModified('password')) return next();
|
|
49
|
+
this.password = await bcrypt.hash(this.password, 10);
|
|
50
|
+
next();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// 密码验证
|
|
54
|
+
userSchema.methods.comparePassword = async function(candidatePassword) {
|
|
55
|
+
return await bcrypt.compare(candidatePassword, this.password);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default mongoose.model('User', userSchema);
|
|
59
|
+
|
|
60
|
+
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { authenticate, isAdmin } from '../middleware/auth.js';
|
|
3
|
+
import {
|
|
4
|
+
queryAuditLogs,
|
|
5
|
+
getUserActivityStats,
|
|
6
|
+
getDocumentEditHistory
|
|
7
|
+
} from '../utils/auditLogger.js';
|
|
8
|
+
|
|
9
|
+
const router = express.Router();
|
|
10
|
+
|
|
11
|
+
// 获取审计日志列表(仅管理员)
|
|
12
|
+
router.get('/', authenticate, isAdmin, async (req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
const {
|
|
15
|
+
page = 1,
|
|
16
|
+
limit = 50,
|
|
17
|
+
userId,
|
|
18
|
+
resourceId,
|
|
19
|
+
resourceType,
|
|
20
|
+
action,
|
|
21
|
+
groupId,
|
|
22
|
+
startDate,
|
|
23
|
+
endDate,
|
|
24
|
+
sortBy = 'createdAt',
|
|
25
|
+
sortOrder = -1
|
|
26
|
+
} = req.query;
|
|
27
|
+
|
|
28
|
+
const filters = {};
|
|
29
|
+
if (userId) filters.userId = userId;
|
|
30
|
+
if (resourceId) filters.resourceId = resourceId;
|
|
31
|
+
if (resourceType) filters.resourceType = resourceType;
|
|
32
|
+
if (action) filters.action = action;
|
|
33
|
+
if (groupId) filters.groupId = groupId;
|
|
34
|
+
if (startDate) filters.startDate = startDate;
|
|
35
|
+
if (endDate) filters.endDate = endDate;
|
|
36
|
+
|
|
37
|
+
const options = {
|
|
38
|
+
page: parseInt(page),
|
|
39
|
+
limit: parseInt(limit),
|
|
40
|
+
sortBy,
|
|
41
|
+
sortOrder: parseInt(sortOrder)
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const result = await queryAuditLogs(filters, options);
|
|
45
|
+
|
|
46
|
+
res.json({
|
|
47
|
+
message: '获取审计日志成功',
|
|
48
|
+
...result
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('获取审计日志失败:', error);
|
|
52
|
+
res.status(500).json({
|
|
53
|
+
message: '获取审计日志失败',
|
|
54
|
+
error: error.message
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// 获取特定用户的活动统计(仅管理员)
|
|
60
|
+
router.get('/user-stats/:userId', authenticate, isAdmin, async (req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
const { userId } = req.params;
|
|
63
|
+
const { startDate, endDate } = req.query;
|
|
64
|
+
|
|
65
|
+
const timeRange = {};
|
|
66
|
+
if (startDate) timeRange.startDate = startDate;
|
|
67
|
+
if (endDate) timeRange.endDate = endDate;
|
|
68
|
+
|
|
69
|
+
const stats = await getUserActivityStats(userId, timeRange);
|
|
70
|
+
|
|
71
|
+
res.json({
|
|
72
|
+
message: '获取用户活动统计成功',
|
|
73
|
+
stats
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('获取用户活动统计失败:', error);
|
|
77
|
+
res.status(500).json({
|
|
78
|
+
message: '获取用户活动统计失败',
|
|
79
|
+
error: error.message
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// 获取文档编辑历史(仅管理员)
|
|
85
|
+
router.get('/document-history/:documentId', authenticate, isAdmin, async (req, res) => {
|
|
86
|
+
try {
|
|
87
|
+
const { documentId } = req.params;
|
|
88
|
+
const { limit = 20 } = req.query;
|
|
89
|
+
|
|
90
|
+
const history = await getDocumentEditHistory(documentId, parseInt(limit));
|
|
91
|
+
|
|
92
|
+
res.json({
|
|
93
|
+
message: '获取文档编辑历史成功',
|
|
94
|
+
history
|
|
95
|
+
});
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('获取文档编辑历史失败:', error);
|
|
98
|
+
res.status(500).json({
|
|
99
|
+
message: '获取文档编辑历史失败',
|
|
100
|
+
error: error.message
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// 获取群组活动日志(仅管理员)
|
|
106
|
+
router.get('/group/:groupId', authenticate, isAdmin, async (req, res) => {
|
|
107
|
+
try {
|
|
108
|
+
const { groupId } = req.params;
|
|
109
|
+
const {
|
|
110
|
+
page = 1,
|
|
111
|
+
limit = 50,
|
|
112
|
+
action,
|
|
113
|
+
userId,
|
|
114
|
+
startDate,
|
|
115
|
+
endDate
|
|
116
|
+
} = req.query;
|
|
117
|
+
|
|
118
|
+
const filters = { groupId };
|
|
119
|
+
if (action) filters.action = action;
|
|
120
|
+
if (userId) filters.userId = userId;
|
|
121
|
+
if (startDate) filters.startDate = startDate;
|
|
122
|
+
if (endDate) filters.endDate = endDate;
|
|
123
|
+
|
|
124
|
+
const options = {
|
|
125
|
+
page: parseInt(page),
|
|
126
|
+
limit: parseInt(limit),
|
|
127
|
+
sortBy: 'createdAt',
|
|
128
|
+
sortOrder: -1
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const result = await queryAuditLogs(filters, options);
|
|
132
|
+
|
|
133
|
+
res.json({
|
|
134
|
+
message: '获取群组活动日志成功',
|
|
135
|
+
...result
|
|
136
|
+
});
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('获取群组活动日志失败:', error);
|
|
139
|
+
res.status(500).json({
|
|
140
|
+
message: '获取群组活动日志失败',
|
|
141
|
+
error: error.message
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// 获取审计日志统计信息(仅管理员)
|
|
147
|
+
router.get('/stats/summary', authenticate, isAdmin, async (req, res) => {
|
|
148
|
+
try {
|
|
149
|
+
const { startDate, endDate, groupId } = req.query;
|
|
150
|
+
|
|
151
|
+
const filters = {};
|
|
152
|
+
if (groupId) filters.groupId = groupId;
|
|
153
|
+
if (startDate || endDate) {
|
|
154
|
+
filters.startDate = startDate;
|
|
155
|
+
filters.endDate = endDate;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 获取总体统计
|
|
159
|
+
const result = await queryAuditLogs(filters, { limit: 1 });
|
|
160
|
+
const totalLogs = result.pagination.total;
|
|
161
|
+
|
|
162
|
+
// 获取按操作类型统计
|
|
163
|
+
const actionStats = await queryAuditLogs(filters, { limit: totalLogs });
|
|
164
|
+
const actionBreakdown = {};
|
|
165
|
+
|
|
166
|
+
actionStats.logs.forEach(log => {
|
|
167
|
+
actionBreakdown[log.action] = (actionBreakdown[log.action] || 0) + 1;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// 获取最活跃用户
|
|
171
|
+
const userActivity = {};
|
|
172
|
+
actionStats.logs.forEach(log => {
|
|
173
|
+
const userId = log.user._id.toString();
|
|
174
|
+
if (!userActivity[userId]) {
|
|
175
|
+
userActivity[userId] = {
|
|
176
|
+
user: log.user,
|
|
177
|
+
count: 0
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
userActivity[userId].count++;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const topUsers = Object.values(userActivity)
|
|
184
|
+
.sort((a, b) => b.count - a.count)
|
|
185
|
+
.slice(0, 10);
|
|
186
|
+
|
|
187
|
+
res.json({
|
|
188
|
+
message: '获取统计信息成功',
|
|
189
|
+
summary: {
|
|
190
|
+
totalLogs,
|
|
191
|
+
actionBreakdown,
|
|
192
|
+
topUsers,
|
|
193
|
+
timeRange: { startDate, endDate }
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error('获取统计信息失败:', error);
|
|
198
|
+
res.status(500).json({
|
|
199
|
+
message: '获取统计信息失败',
|
|
200
|
+
error: error.message
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
export default router;
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|