198 lines
6.5 KiB
TypeScript
198 lines
6.5 KiB
TypeScript
|
|
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]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|