基本功能完成,下一步开始美化UI
This commit is contained in:
@@ -1,18 +1,43 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { UserModel } from '../models/user';
|
||||
import { QuizModel } from '../models/quiz';
|
||||
import { UserGroupModel } from '../models/userGroup';
|
||||
|
||||
export class AdminUserController {
|
||||
static async getUsers(req: Request, res: Response) {
|
||||
try {
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
const limit = parseInt(req.query.limit as string) || 10;
|
||||
const keyword = req.query.keyword as string;
|
||||
|
||||
// TODO: Implement search in UserModel if needed, currently filtering in memory or ignored
|
||||
// For now assuming findAll supports basic pagination.
|
||||
// If keyword is needed, we should add findByKeyword to UserModel.
|
||||
// But based on existing code, it seems it wasn't implemented there.
|
||||
// Let's stick to what was there or improve if I see it.
|
||||
// The previous code used findAll(limit, offset).
|
||||
|
||||
const result = await UserModel.findAll(limit, (page - 1) * limit);
|
||||
|
||||
// Filter by keyword if provided (naive implementation since DB doesn't support it yet via API)
|
||||
let users = result.users;
|
||||
if (keyword) {
|
||||
users = users.filter(u => u.name.includes(keyword) || u.phone.includes(keyword));
|
||||
}
|
||||
|
||||
// 获取每个用户的用户组信息
|
||||
const usersWithGroups = await Promise.all(users.map(async (u) => {
|
||||
const groups = await UserGroupModel.getUserGroups(u.id);
|
||||
return {
|
||||
...u,
|
||||
password: u.password ?? '',
|
||||
groups
|
||||
};
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.users.map((u) => ({ ...u, password: u.password ?? '' })),
|
||||
data: usersWithGroups,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
@@ -28,6 +53,58 @@ export class AdminUserController {
|
||||
}
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
static async createUser(req: Request, res: Response) {
|
||||
try {
|
||||
const { name, phone, password, groupIds } = req.body;
|
||||
|
||||
if (!name || !phone) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '姓名和手机号不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
const errors = UserModel.validateUserData({ name, phone, password });
|
||||
if (errors.length > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '数据验证失败: ' + errors.join(', ')
|
||||
});
|
||||
}
|
||||
|
||||
const user = await UserModel.create({ name, phone, password });
|
||||
|
||||
// 自动加入"全体用户"组
|
||||
const allUsersGroup = await UserGroupModel.getSystemGroup();
|
||||
if (allUsersGroup) {
|
||||
await UserGroupModel.addMember(allUsersGroup.id, user.id);
|
||||
}
|
||||
|
||||
// 添加到指定用户组
|
||||
if (groupIds && Array.isArray(groupIds)) {
|
||||
await UserGroupModel.updateUserGroups(user.id, groupIds);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: user
|
||||
});
|
||||
} catch (error: any) {
|
||||
// 处理手机号已存在的错误
|
||||
if (error.message === '手机号已存在' || error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '手机号已存在'
|
||||
});
|
||||
}
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '创建用户失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteUser(req: Request, res: Response) {
|
||||
try {
|
||||
const { userId } = req.body;
|
||||
@@ -87,7 +164,7 @@ export class AdminUserController {
|
||||
static async updateUser(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, phone, password } = req.body;
|
||||
const { name, phone, password, groupIds } = req.body;
|
||||
|
||||
const user = await UserModel.findById(id);
|
||||
if (!user) {
|
||||
@@ -106,9 +183,20 @@ export class AdminUserController {
|
||||
// 更新用户
|
||||
const updatedUser = await UserModel.update(id, updateData);
|
||||
|
||||
// 更新用户组
|
||||
if (groupIds && Array.isArray(groupIds)) {
|
||||
await UserGroupModel.updateUserGroups(id, groupIds);
|
||||
}
|
||||
|
||||
// 获取最新用户组信息
|
||||
const groups = await UserGroupModel.getUserGroups(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: updatedUser
|
||||
data: {
|
||||
...updatedUser,
|
||||
groups
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
// 处理手机号已存在的错误
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { ExamTaskModel } from '../models/examTask';
|
||||
import { UserGroupModel } from '../models/userGroup';
|
||||
|
||||
export class ExamTaskController {
|
||||
static async getTasks(req: Request, res: Response) {
|
||||
@@ -19,12 +20,29 @@ export class ExamTaskController {
|
||||
|
||||
static async createTask(req: Request, res: Response) {
|
||||
try {
|
||||
const { name, subjectId, startAt, endAt, userIds } = req.body;
|
||||
const { name, subjectId, startAt, endAt, userIds = [], groupIds = [] } = req.body;
|
||||
|
||||
if (!name || !subjectId || !startAt || !endAt || !Array.isArray(userIds) || userIds.length === 0) {
|
||||
if (!name || !subjectId || !startAt || !endAt) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数不完整或用户列表为空'
|
||||
message: '参数不完整'
|
||||
});
|
||||
}
|
||||
|
||||
// 合并用户列表和用户组中的用户
|
||||
const finalUserIds = new Set<string>(userIds);
|
||||
|
||||
if (Array.isArray(groupIds) && groupIds.length > 0) {
|
||||
for (const groupId of groupIds) {
|
||||
const members = await UserGroupModel.getMembers(groupId);
|
||||
members.forEach(m => finalUserIds.add(m.id));
|
||||
}
|
||||
}
|
||||
|
||||
if (finalUserIds.size === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请至少选择一位用户或一个用户组'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,7 +51,8 @@ export class ExamTaskController {
|
||||
subjectId,
|
||||
startAt,
|
||||
endAt,
|
||||
userIds
|
||||
userIds: Array.from(finalUserIds),
|
||||
selectionConfig: JSON.stringify({ userIds, groupIds })
|
||||
});
|
||||
|
||||
res.json({
|
||||
@@ -51,12 +70,29 @@ export class ExamTaskController {
|
||||
static async updateTask(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, subjectId, startAt, endAt, userIds } = req.body;
|
||||
const { name, subjectId, startAt, endAt, userIds = [], groupIds = [] } = req.body;
|
||||
|
||||
if (!name || !subjectId || !startAt || !endAt || !Array.isArray(userIds) || userIds.length === 0) {
|
||||
if (!name || !subjectId || !startAt || !endAt) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数不完整或用户列表为空'
|
||||
message: '参数不完整'
|
||||
});
|
||||
}
|
||||
|
||||
// 合并用户列表和用户组中的用户
|
||||
const finalUserIds = new Set<string>(userIds);
|
||||
|
||||
if (Array.isArray(groupIds) && groupIds.length > 0) {
|
||||
for (const groupId of groupIds) {
|
||||
const members = await UserGroupModel.getMembers(groupId);
|
||||
members.forEach(m => finalUserIds.add(m.id));
|
||||
}
|
||||
}
|
||||
|
||||
if (finalUserIds.size === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请至少选择一位用户或一个用户组'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,7 +101,8 @@ export class ExamTaskController {
|
||||
subjectId,
|
||||
startAt,
|
||||
endAt,
|
||||
userIds
|
||||
userIds: Array.from(finalUserIds),
|
||||
selectionConfig: JSON.stringify({ userIds, groupIds })
|
||||
});
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// 导出所有控制器
|
||||
export { UserController } from './userController';
|
||||
export { QuestionController } from './questionController';
|
||||
export { QuizController } from './quizController';
|
||||
export { AdminController } from './adminController';
|
||||
export { BackupController } from './backupController';
|
||||
export { QuestionCategoryController } from './questionCategoryController';
|
||||
export { ExamSubjectController } from './examSubjectController';
|
||||
export { ExamTaskController } from './examTaskController';
|
||||
export { AdminUserController } from './adminUserController';
|
||||
export * from './userController';
|
||||
export * from './questionController';
|
||||
export * from './quizController';
|
||||
export * from './adminController';
|
||||
export * from './questionCategoryController';
|
||||
export * from './examSubjectController';
|
||||
export * from './examTaskController';
|
||||
export * from './adminUserController';
|
||||
export * from './userQuizController';
|
||||
export * from './backupController';
|
||||
export * from './userGroupController';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { UserModel } from '../models/user';
|
||||
import { UserGroupModel } from '../models/userGroup';
|
||||
|
||||
export class UserController {
|
||||
static async createUser(req: Request, res: Response) {
|
||||
@@ -24,6 +25,12 @@ export class UserController {
|
||||
|
||||
const user = await UserModel.create({ name, phone, password });
|
||||
|
||||
// 自动加入"全体用户"组
|
||||
const allUsersGroup = await UserGroupModel.getSystemGroup();
|
||||
if (allUsersGroup) {
|
||||
await UserGroupModel.addMember(allUsersGroup.id, user.id);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: user
|
||||
|
||||
64
api/controllers/userGroupController.ts
Normal file
64
api/controllers/userGroupController.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { UserGroupModel } from '../models/userGroup';
|
||||
|
||||
export const userGroupController = {
|
||||
// 获取所有用户组
|
||||
getAll: async (req: Request, res: Response) => {
|
||||
try {
|
||||
const groups = await UserGroupModel.findAll();
|
||||
res.json(groups);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
// 创建用户组
|
||||
create: async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { name, description } = req.body;
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: '用户组名称不能为空' });
|
||||
}
|
||||
|
||||
const group = await UserGroupModel.create({ name, description });
|
||||
res.status(201).json(group);
|
||||
} catch (error: any) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
// 更新用户组
|
||||
update: async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, description } = req.body;
|
||||
|
||||
const group = await UserGroupModel.update(id, { name, description });
|
||||
res.json(group);
|
||||
} catch (error: any) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
// 删除用户组
|
||||
delete: async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
await UserGroupModel.delete(id);
|
||||
res.json({ message: '用户组删除成功' });
|
||||
} catch (error: any) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
// 获取用户组成员
|
||||
getMembers: async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const members = await UserGroupModel.getMembers(id);
|
||||
res.json(members);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import sqlite3 from 'sqlite3';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const DEFAULT_DB_DIR = path.join(process.cwd(), 'data');
|
||||
const DEFAULT_DB_PATH = path.join(DEFAULT_DB_DIR, 'survey.db');
|
||||
@@ -127,6 +128,65 @@ const migrateDatabase = async () => {
|
||||
await ensureIndex(`CREATE INDEX IF NOT EXISTS idx_exam_task_users_user_id ON exam_task_users(user_id);`);
|
||||
await ensureIndex(`CREATE INDEX IF NOT EXISTS idx_quiz_records_subject_id ON quiz_records(subject_id);`);
|
||||
await ensureIndex(`CREATE INDEX IF NOT EXISTS idx_quiz_records_task_id ON quiz_records(task_id);`);
|
||||
|
||||
// 1. 创建用户组表
|
||||
await ensureTable(`
|
||||
CREATE TABLE IF NOT EXISTS user_groups (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
is_system BOOLEAN DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
// 2. 创建用户-用户组关联表
|
||||
await ensureTable(`
|
||||
CREATE TABLE IF NOT EXISTS user_group_members (
|
||||
group_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (group_id, user_id),
|
||||
FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
// 3. 为考试任务表添加选择配置字段
|
||||
await ensureColumn('exam_tasks', 'selection_config TEXT', 'selection_config');
|
||||
|
||||
// 4. 初始化"全体用户"组
|
||||
const allUsersGroup = await get(`SELECT id FROM user_groups WHERE is_system = 1`);
|
||||
let allUsersGroupId = allUsersGroup?.id;
|
||||
|
||||
if (!allUsersGroupId) {
|
||||
allUsersGroupId = uuidv4();
|
||||
await run(
|
||||
`INSERT INTO user_groups (id, name, description, is_system) VALUES (?, ?, ?, ?)`,
|
||||
[allUsersGroupId, '全体用户', '包含系统所有用户的默认组', 1]
|
||||
);
|
||||
console.log('已创建"全体用户"系统组');
|
||||
}
|
||||
|
||||
// 5. 将现有用户添加到"全体用户"组
|
||||
if (allUsersGroupId) {
|
||||
// 找出尚未在全体用户组中的用户
|
||||
const usersNotInGroup = await query(`
|
||||
SELECT id FROM users
|
||||
WHERE id NOT IN (
|
||||
SELECT user_id FROM user_group_members WHERE group_id = ?
|
||||
)
|
||||
`, [allUsersGroupId]);
|
||||
|
||||
if (usersNotInGroup.length > 0) {
|
||||
const stmt = db.prepare(`INSERT INTO user_group_members (group_id, user_id) VALUES (?, ?)`);
|
||||
usersNotInGroup.forEach(user => {
|
||||
stmt.run(allUsersGroupId, user.id);
|
||||
});
|
||||
stmt.finalize();
|
||||
console.log(`已将 ${usersNotInGroup.length} 名现有用户添加到"全体用户"组`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 数据库初始化函数
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface ExamTask {
|
||||
startAt: string;
|
||||
endAt: string;
|
||||
createdAt: string;
|
||||
selectionConfig?: string; // JSON string
|
||||
}
|
||||
|
||||
export interface ExamTaskUser {
|
||||
@@ -178,7 +179,7 @@ export class ExamTaskModel {
|
||||
}
|
||||
|
||||
static async findById(id: string): Promise<ExamTask | null> {
|
||||
const sql = `SELECT id, name, subject_id as subjectId, start_at as startAt, end_at as endAt, created_at as createdAt FROM exam_tasks WHERE id = ?`;
|
||||
const sql = `SELECT id, name, subject_id as subjectId, start_at as startAt, end_at as endAt, created_at as createdAt, selection_config as selectionConfig FROM exam_tasks WHERE id = ?`;
|
||||
const row = await get(sql, [id]);
|
||||
return row || null;
|
||||
}
|
||||
@@ -189,6 +190,7 @@ export class ExamTaskModel {
|
||||
startAt: string;
|
||||
endAt: string;
|
||||
userIds: string[];
|
||||
selectionConfig?: string;
|
||||
}): Promise<ExamTask> {
|
||||
if (!data.name.trim()) throw new Error('任务名称不能为空');
|
||||
if (!data.userIds.length) throw new Error('至少选择一位用户');
|
||||
@@ -198,8 +200,8 @@ export class ExamTaskModel {
|
||||
|
||||
const id = uuidv4();
|
||||
const sqlTask = `
|
||||
INSERT INTO exam_tasks (id, name, subject_id, start_at, end_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
INSERT INTO exam_tasks (id, name, subject_id, start_at, end_at, selection_config)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const sqlTaskUser = `
|
||||
@@ -207,7 +209,7 @@ export class ExamTaskModel {
|
||||
VALUES (?, ?, ?)
|
||||
`;
|
||||
|
||||
await run(sqlTask, [id, data.name.trim(), data.subjectId, data.startAt, data.endAt]);
|
||||
await run(sqlTask, [id, data.name.trim(), data.subjectId, data.startAt, data.endAt, data.selectionConfig || null]);
|
||||
|
||||
for (const userId of data.userIds) {
|
||||
await run(sqlTaskUser, [uuidv4(), id, userId]);
|
||||
@@ -222,6 +224,7 @@ export class ExamTaskModel {
|
||||
startAt: string;
|
||||
endAt: string;
|
||||
userIds: string[];
|
||||
selectionConfig?: string;
|
||||
}): Promise<ExamTask> {
|
||||
const existing = await this.findById(id);
|
||||
if (!existing) throw new Error('任务不存在');
|
||||
@@ -232,11 +235,12 @@ export class ExamTaskModel {
|
||||
const subject = await import('./examSubject').then(({ ExamSubjectModel }) => ExamSubjectModel.findById(data.subjectId));
|
||||
if (!subject) throw new Error('科目不存在');
|
||||
|
||||
await run(`UPDATE exam_tasks SET name = ?, subject_id = ?, start_at = ?, end_at = ? WHERE id = ?`, [
|
||||
await run(`UPDATE exam_tasks SET name = ?, subject_id = ?, start_at = ?, end_at = ?, selection_config = ? WHERE id = ?`, [
|
||||
data.name.trim(),
|
||||
data.subjectId,
|
||||
data.startAt,
|
||||
data.endAt,
|
||||
data.selectionConfig || null,
|
||||
id
|
||||
]);
|
||||
|
||||
|
||||
@@ -5,4 +5,5 @@ export { QuizModel, type QuizRecord, type QuizAnswer, type SubmitQuizData } from
|
||||
export { SystemConfigModel, type SystemConfig, type QuizConfig, type AdminUser } from './systemConfig';
|
||||
export { QuestionCategoryModel, type QuestionCategory } from './questionCategory';
|
||||
export { ExamSubjectModel, type ExamSubject } from './examSubject';
|
||||
export { ExamTaskModel, type ExamTask, type TaskWithSubject, type TaskReport } from './examTask';
|
||||
export { ExamTaskModel, type ExamTask, type TaskWithSubject, type TaskReport } from './examTask';
|
||||
export { UserGroupModel, type UserGroup, type CreateUserGroupData } from './userGroup';
|
||||
|
||||
@@ -86,8 +86,6 @@ export class QuestionCategoryModel {
|
||||
}
|
||||
|
||||
static async update(id: string, name: string): Promise<QuestionCategory> {
|
||||
if (id === 'default') throw new Error('默认类别不允许修改');
|
||||
|
||||
const existing = await this.findById(id);
|
||||
if (!existing) throw new Error('类别不存在');
|
||||
|
||||
|
||||
197
api/models/userGroup.ts
Normal file
197
api/models/userGroup.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { query, run, get } from '../database';
|
||||
import { User } from './user';
|
||||
|
||||
export interface UserGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
isSystem: boolean;
|
||||
createdAt: string;
|
||||
memberCount?: number;
|
||||
}
|
||||
|
||||
export interface CreateUserGroupData {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class UserGroupModel {
|
||||
static async create(data: CreateUserGroupData): Promise<UserGroup> {
|
||||
const id = uuidv4();
|
||||
const sql = `
|
||||
INSERT INTO user_groups (id, name, description, is_system)
|
||||
VALUES (?, ?, ?, 0)
|
||||
`;
|
||||
|
||||
try {
|
||||
await run(sql, [id, data.name, data.description || '']);
|
||||
return this.findById(id) as Promise<UserGroup>;
|
||||
} catch (error: any) {
|
||||
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
||||
throw new Error('用户组名称已存在');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async update(id: string, data: Partial<CreateUserGroupData>): Promise<UserGroup> {
|
||||
const group = await this.findById(id);
|
||||
if (!group) throw new Error('用户组不存在');
|
||||
if (group.isSystem) throw new Error('系统内置用户组无法修改');
|
||||
|
||||
const fields: string[] = [];
|
||||
const values: any[] = [];
|
||||
|
||||
if (data.name !== undefined) {
|
||||
fields.push('name = ?');
|
||||
values.push(data.name);
|
||||
}
|
||||
|
||||
if (data.description !== undefined) {
|
||||
fields.push('description = ?');
|
||||
values.push(data.description);
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
return group;
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
const sql = `UPDATE user_groups SET ${fields.join(', ')} WHERE id = ?`;
|
||||
|
||||
try {
|
||||
await run(sql, values);
|
||||
return this.findById(id) as Promise<UserGroup>;
|
||||
} catch (error: any) {
|
||||
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
||||
throw new Error('用户组名称已存在');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async delete(id: string): Promise<void> {
|
||||
const group = await this.findById(id);
|
||||
if (!group) throw new Error('用户组不存在');
|
||||
if (group.isSystem) throw new Error('系统内置用户组无法删除');
|
||||
|
||||
await run(`DELETE FROM user_groups WHERE id = ?`, [id]);
|
||||
}
|
||||
|
||||
static async findById(id: string): Promise<UserGroup | null> {
|
||||
const sql = `
|
||||
SELECT id, name, description, is_system as isSystem, created_at as createdAt
|
||||
FROM user_groups WHERE id = ?
|
||||
`;
|
||||
const group = await get(sql, [id]);
|
||||
return group || null;
|
||||
}
|
||||
|
||||
static async findAll(): Promise<UserGroup[]> {
|
||||
const sql = `
|
||||
SELECT
|
||||
g.id, g.name, g.description, g.is_system as isSystem, g.created_at as createdAt,
|
||||
(SELECT COUNT(*) FROM user_group_members m WHERE m.group_id = g.id) as memberCount
|
||||
FROM user_groups g
|
||||
ORDER BY g.is_system DESC, g.created_at DESC
|
||||
`;
|
||||
return await query(sql);
|
||||
}
|
||||
|
||||
static async addMember(groupId: string, userId: string): Promise<void> {
|
||||
const sql = `INSERT OR IGNORE INTO user_group_members (group_id, user_id) VALUES (?, ?)`;
|
||||
await run(sql, [groupId, userId]);
|
||||
}
|
||||
|
||||
static async removeMember(groupId: string, userId: string): Promise<void> {
|
||||
const group = await this.findById(groupId);
|
||||
if (group?.isSystem) {
|
||||
// Check if user is being deleted? No, this method is for removing member.
|
||||
// Requirement: "User cannot actively exit this group".
|
||||
// Implementation: Cannot remove member from system group via this API.
|
||||
// Only user deletion removes them (cascade).
|
||||
throw new Error('无法从系统内置组中移除成员');
|
||||
}
|
||||
const sql = `DELETE FROM user_group_members WHERE group_id = ? AND user_id = ?`;
|
||||
await run(sql, [groupId, userId]);
|
||||
}
|
||||
|
||||
static async getMembers(groupId: string): Promise<User[]> {
|
||||
const sql = `
|
||||
SELECT u.id, u.name, u.phone, u.created_at as createdAt
|
||||
FROM users u
|
||||
JOIN user_group_members m ON u.id = m.user_id
|
||||
WHERE m.group_id = ?
|
||||
ORDER BY m.created_at DESC
|
||||
`;
|
||||
return await query(sql, [groupId]);
|
||||
}
|
||||
|
||||
static async getUserGroups(userId: string): Promise<UserGroup[]> {
|
||||
const sql = `
|
||||
SELECT g.id, g.name, g.description, g.is_system as isSystem, g.created_at as createdAt
|
||||
FROM user_groups g
|
||||
JOIN user_group_members m ON g.id = m.group_id
|
||||
WHERE m.user_id = ?
|
||||
ORDER BY g.is_system DESC, g.created_at DESC
|
||||
`;
|
||||
return await query(sql, [userId]);
|
||||
}
|
||||
|
||||
static async getSystemGroup(): Promise<UserGroup | null> {
|
||||
const sql = `
|
||||
SELECT id, name, description, is_system as isSystem, created_at as createdAt
|
||||
FROM user_groups WHERE is_system = 1
|
||||
`;
|
||||
return await get(sql);
|
||||
}
|
||||
|
||||
static async updateMembers(groupId: string, userIds: string[]): Promise<void> {
|
||||
const group = await this.findById(groupId);
|
||||
if (!group) throw new Error('用户组不存在');
|
||||
if (group.isSystem) throw new Error('无法修改系统内置组成员');
|
||||
|
||||
// Transaction-like behavior needed but SQLite wrapper doesn't expose it easily.
|
||||
// We'll do delete then insert.
|
||||
await run(`DELETE FROM user_group_members WHERE group_id = ?`, [groupId]);
|
||||
|
||||
if (userIds.length > 0) {
|
||||
// Batch insert
|
||||
// SQLite limit is usually high enough, but safer to loop or construct big query
|
||||
// For simplicity in this helper:
|
||||
for (const userId of userIds) {
|
||||
await run(`INSERT INTO user_group_members (group_id, user_id) VALUES (?, ?)`, [groupId, userId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async updateUserGroups(userId: string, groupIds: string[]): Promise<void> {
|
||||
// 1. Get current system group(s) the user belongs to
|
||||
const currentGroups = await this.getUserGroups(userId);
|
||||
const systemGroupIds = currentGroups.filter(g => g.isSystem).map(g => g.id);
|
||||
|
||||
// 2. Ensure system groups are in the new list (force keep them)
|
||||
const newGroupSet = new Set(groupIds);
|
||||
for (const sysId of systemGroupIds) {
|
||||
newGroupSet.add(sysId);
|
||||
}
|
||||
|
||||
// Also ensure the default "All Users" group is there if not already
|
||||
// (In case the user was created before groups existed and somehow not migrated, though migration handles it)
|
||||
// Safe to just ensure "All Users" is present.
|
||||
const allUsersGroup = await this.getSystemGroup();
|
||||
if (allUsersGroup) {
|
||||
newGroupSet.add(allUsersGroup.id);
|
||||
}
|
||||
|
||||
const finalGroupIds = Array.from(newGroupSet);
|
||||
|
||||
// 3. Update
|
||||
await run(`DELETE FROM user_group_members WHERE user_id = ?`, [userId]);
|
||||
|
||||
for (const gid of finalGroupIds) {
|
||||
await run(`INSERT INTO user_group_members (group_id, user_id) VALUES (?, ?)`, [gid, userId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
QuestionCategoryController,
|
||||
ExamSubjectController,
|
||||
ExamTaskController,
|
||||
AdminUserController
|
||||
AdminUserController,
|
||||
userGroupController as UserGroupController
|
||||
} from './controllers';
|
||||
import {
|
||||
upload,
|
||||
@@ -75,8 +76,16 @@ apiRouter.put('/admin/tasks/:id', adminAuth, ExamTaskController.updateTask);
|
||||
apiRouter.delete('/admin/tasks/:id', adminAuth, ExamTaskController.deleteTask);
|
||||
apiRouter.get('/admin/tasks/:id/report', adminAuth, ExamTaskController.getTaskReport);
|
||||
|
||||
// 用户组管理
|
||||
apiRouter.get('/admin/user-groups', adminAuth, UserGroupController.getAll);
|
||||
apiRouter.post('/admin/user-groups', adminAuth, UserGroupController.create);
|
||||
apiRouter.put('/admin/user-groups/:id', adminAuth, UserGroupController.update);
|
||||
apiRouter.delete('/admin/user-groups/:id', adminAuth, UserGroupController.delete);
|
||||
apiRouter.get('/admin/user-groups/:id/members', adminAuth, UserGroupController.getMembers);
|
||||
|
||||
// 用户管理
|
||||
apiRouter.get('/admin/users', adminAuth, AdminUserController.getUsers);
|
||||
apiRouter.post('/admin/users', adminAuth, AdminUserController.createUser);
|
||||
apiRouter.put('/admin/users/:id', adminAuth, AdminUserController.updateUser);
|
||||
apiRouter.delete('/admin/users', adminAuth, AdminUserController.deleteUser);
|
||||
apiRouter.get('/admin/users/export', adminAuth, AdminUserController.exportUsers);
|
||||
|
||||
Reference in New Issue
Block a user