/** * 用户管理服务 * @description 提供用户 CRUD、租户隔离、模块权限管理等功能 */ import { PrismaClient, UserRole, Prisma } from '@prisma/client'; import bcrypt from 'bcryptjs'; import { v4 as uuidv4 } from 'uuid'; import { logger } from '../../../common/logging/index.js'; import { ListUsersQuery, CreateUserRequest, UpdateUserRequest, AssignTenantRequest, UpdateUserModulesRequest, UserListItem, UserDetail, TenantMembership, PaginatedResponse, UserQueryScope, ImportUserRow, ImportResult, ImportError, } from '../types/user.types.js'; const prisma = new PrismaClient(); // 默认密码 const DEFAULT_PASSWORD = '123456'; /** * 根据用户ID获取用户的 departmentId */ export async function getUserDepartmentId(userId: string): Promise { const user = await prisma.user.findUnique({ where: { id: userId }, select: { department_id: true }, }); return user?.department_id || undefined; } /** * 根据用户角色获取查询范围 */ export async function getUserQueryScope( userRole: UserRole | string, tenantId?: string, userId?: string ): Promise { switch (userRole) { case 'SUPER_ADMIN': case 'PROMPT_ENGINEER': return {}; // 无限制 case 'HOSPITAL_ADMIN': case 'PHARMA_ADMIN': return { tenantId }; // 只能查看本租户 case 'DEPARTMENT_ADMIN': { // 科室主任需要查询其 departmentId const departmentId = userId ? await getUserDepartmentId(userId) : undefined; return { tenantId, departmentId }; } default: throw new Error('无权限访问用户管理'); } } /** * 获取用户列表(支持分页、搜索、筛选) */ export async function listUsers( query: ListUsersQuery, scope: UserQueryScope ): Promise> { // 确保 page 和 pageSize 是数字类型(HTTP 查询参数默认是字符串) const page = Number(query.page) || 1; const pageSize = Number(query.pageSize) || 20; const { search, role, tenantId, status, departmentId } = query; const skip = (page - 1) * pageSize; // 构建查询条件 const where: Prisma.UserWhereInput = {}; // 数据隔离:根据 scope 限制查询范围 if (scope.tenantId) { where.tenant_id = scope.tenantId; } if (scope.departmentId) { where.department_id = scope.departmentId; } // 搜索条件 if (search) { where.OR = [ { phone: { contains: search } }, { name: { contains: search } }, { email: { contains: search } }, ]; } // 筛选条件 if (role) { where.role = role; } if (tenantId && !scope.tenantId) { // 只有无限制 scope 才能按租户筛选 where.tenant_id = tenantId; } if (status) { where.status = status; } if (departmentId && !scope.departmentId) { where.department_id = departmentId; } // 查询总数 const total = await prisma.user.count({ where }); // 查询列表 const users = await prisma.user.findMany({ where, skip, take: pageSize, orderBy: { createdAt: 'desc' }, include: { tenants: { select: { id: true, code: true, name: true, type: true, }, }, departments: { select: { id: true, name: true, }, }, tenant_members: { select: { id: true, }, }, }, }); // 转换为列表项 const data: UserListItem[] = users.map((user) => ({ id: user.id, phone: user.phone, name: user.name, email: user.email, role: user.role, status: user.status, isDefaultPassword: user.is_default_password, defaultTenant: { id: user.tenants.id, code: user.tenants.code, name: user.tenants.name, type: user.tenants.type, }, department: user.departments ? { id: user.departments.id, name: user.departments.name, } : null, tenantCount: user.tenant_members.length, createdAt: user.createdAt, lastLoginAt: user.lastLoginAt, })); return { data, total, page, pageSize, totalPages: Math.ceil(total / pageSize), }; } /** * 获取用户详情 */ export async function getUserById(userId: string, scope: UserQueryScope): Promise { const where: Prisma.UserWhereInput = { id: userId }; // 数据隔离 if (scope.tenantId) { where.tenant_id = scope.tenantId; } const user = await prisma.user.findFirst({ where, include: { tenants: { select: { id: true, code: true, name: true, type: true, }, }, departments: { select: { id: true, name: true, }, }, tenant_members: { include: { tenants: { select: { id: true, code: true, name: true, type: true, }, }, }, }, user_modules: { include: { tenant: { select: { id: true, code: true, }, }, }, }, }, }); if (!user) { return null; } // 获取每个租户的模块权限 const tenantMemberships: TenantMembership[] = await Promise.all( user.tenant_members.map(async (tm) => { // 获取租户订阅的模块 const tenantModules = await prisma.tenant_modules.findMany({ where: { tenant_id: tm.tenants.id, is_enabled: true }, }); // 获取用户在该租户的模块权限 const userModulesInTenant = user.user_modules.filter( (um) => um.tenant_id === tm.tenants.id ); // 计算最终模块权限 const allowedModules = tenantModules.map((tm) => { const userModule = userModulesInTenant.find((um) => um.module_code === tm.module_code); return { code: tm.module_code, name: getModuleName(tm.module_code), isEnabled: userModule ? userModule.is_enabled : true, // 默认继承租户权限 }; }); return { tenantId: tm.tenants.id, tenantCode: tm.tenants.code, tenantName: tm.tenants.name, tenantType: tm.tenants.type, role: tm.role, joinedAt: tm.joined_at, allowedModules, }; }) ); // 获取用户权限(基于角色) const rolePermissions = await prisma.role_permissions.findMany({ where: { role: user.role }, include: { permissions: true }, }); const permissions = rolePermissions.map((rp) => rp.permissions.code); return { id: user.id, phone: user.phone, name: user.name, email: user.email, role: user.role, status: user.status, isDefaultPassword: user.is_default_password, defaultTenant: { id: user.tenants.id, code: user.tenants.code, name: user.tenants.name, type: user.tenants.type, }, department: user.departments ? { id: user.departments.id, name: user.departments.name, } : null, tenantCount: user.tenant_members.length, createdAt: user.createdAt, lastLoginAt: user.lastLoginAt, tenantMemberships, permissions, }; } /** * 创建用户 */ export async function createUser(data: CreateUserRequest, creatorId: string): Promise { // 检查手机号是否已存在 const existingUser = await prisma.user.findUnique({ where: { phone: data.phone }, }); if (existingUser) { throw new Error('手机号已存在'); } // 检查邮箱是否已存在 if (data.email) { const existingEmail = await prisma.user.findUnique({ where: { email: data.email }, }); if (existingEmail) { throw new Error('邮箱已存在'); } } // 检查租户是否存在 const tenant = await prisma.tenants.findUnique({ where: { id: data.tenantId }, }); if (!tenant) { throw new Error('租户不存在'); } // 检查科室是否存在(如果提供) if (data.departmentId) { const department = await prisma.departments.findFirst({ where: { id: data.departmentId, tenant_id: data.tenantId }, }); if (!department) { throw new Error('科室不存在或不属于该租户'); } } // 加密密码 const hashedPassword = await bcrypt.hash(DEFAULT_PASSWORD, 10); // 创建用户和租户成员关系 const userId = uuidv4(); const tenantMemberId = uuidv4(); const user = await prisma.$transaction(async (tx) => { // 创建用户 const newUser = await tx.user.create({ data: { id: userId, phone: data.phone, name: data.name, email: data.email, password: hashedPassword, role: data.role, tenant_id: data.tenantId, department_id: data.departmentId, is_default_password: true, status: 'active', }, }); // 创建租户成员关系 await tx.tenant_members.create({ data: { id: tenantMemberId, tenant_id: data.tenantId, user_id: userId, role: data.tenantRole || data.role, }, }); // 如果指定了模块权限,创建用户模块记录 if (data.allowedModules && data.allowedModules.length > 0) { await tx.user_modules.createMany({ data: data.allowedModules.map((moduleCode) => ({ id: uuidv4(), user_id: userId, tenant_id: data.tenantId, module_code: moduleCode, is_enabled: true, })), }); } return newUser; }); logger.info('[UserService] User created', { userId: user.id, phone: user.phone, createdBy: creatorId, }); // 返回用户详情 return (await getUserById(user.id, {}))!; } /** * 更新用户 */ export async function updateUser( userId: string, data: UpdateUserRequest, scope: UserQueryScope, updaterId: string ): Promise { // 检查用户是否存在且在权限范围内 const existingUser = await prisma.user.findFirst({ where: { id: userId, ...(scope.tenantId ? { tenant_id: scope.tenantId } : {}), }, }); if (!existingUser) { throw new Error('用户不存在或无权限操作'); } // 检查邮箱唯一性 if (data.email && data.email !== existingUser.email) { const existingEmail = await prisma.user.findUnique({ where: { email: data.email }, }); if (existingEmail) { throw new Error('邮箱已存在'); } } // 更新用户 await prisma.user.update({ where: { id: userId }, data: { name: data.name, email: data.email, role: data.role, department_id: data.departmentId, status: data.status, }, }); logger.info('[UserService] User updated', { userId, updatedFields: Object.keys(data), updatedBy: updaterId, }); return (await getUserById(userId, scope))!; } /** * 更新用户状态(启用/禁用) */ export async function updateUserStatus( userId: string, status: 'active' | 'disabled', scope: UserQueryScope, updaterId: string ): Promise { const existingUser = await prisma.user.findFirst({ where: { id: userId, ...(scope.tenantId ? { tenant_id: scope.tenantId } : {}), }, }); if (!existingUser) { throw new Error('用户不存在或无权限操作'); } await prisma.user.update({ where: { id: userId }, data: { status }, }); logger.info('[UserService] User status updated', { userId, status, updatedBy: updaterId, }); } /** * 重置用户密码 */ export async function resetUserPassword( userId: string, scope: UserQueryScope, resetterId: string ): Promise { const existingUser = await prisma.user.findFirst({ where: { id: userId, ...(scope.tenantId ? { tenant_id: scope.tenantId } : {}), }, }); if (!existingUser) { throw new Error('用户不存在或无权限操作'); } const hashedPassword = await bcrypt.hash(DEFAULT_PASSWORD, 10); await prisma.user.update({ where: { id: userId }, data: { password: hashedPassword, is_default_password: true, password_changed_at: null, }, }); logger.info('[UserService] User password reset', { userId, resetBy: resetterId, }); } /** * 分配租户给用户 */ export async function assignTenantToUser( userId: string, data: AssignTenantRequest, assignerId: string ): Promise { // 检查用户是否存在 const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) { throw new Error('用户不存在'); } // 检查租户是否存在 const tenant = await prisma.tenants.findUnique({ where: { id: data.tenantId } }); if (!tenant) { throw new Error('租户不存在'); } // 检查是否已是该租户成员 const existingMember = await prisma.tenant_members.findUnique({ where: { tenant_id_user_id: { tenant_id: data.tenantId, user_id: userId, }, }, }); if (existingMember) { throw new Error('用户已是该租户成员'); } // 创建租户成员关系 await prisma.$transaction(async (tx) => { await tx.tenant_members.create({ data: { id: uuidv4(), tenant_id: data.tenantId, user_id: userId, role: data.role, }, }); // 如果指定了模块权限,创建用户模块记录 if (data.allowedModules && data.allowedModules.length > 0) { await tx.user_modules.createMany({ data: data.allowedModules.map((moduleCode) => ({ id: uuidv4(), user_id: userId, tenant_id: data.tenantId, module_code: moduleCode, is_enabled: true, })), }); } }); logger.info('[UserService] Tenant assigned to user', { userId, tenantId: data.tenantId, assignedBy: assignerId, }); } /** * 从租户移除用户 */ export async function removeTenantFromUser( userId: string, tenantId: string, removerId: string ): Promise { // 检查用户是否存在 const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) { throw new Error('用户不存在'); } // 不能移除默认租户 if (user.tenant_id === tenantId) { throw new Error('不能移除用户的默认租户'); } // 检查租户成员关系是否存在 const membership = await prisma.tenant_members.findUnique({ where: { tenant_id_user_id: { tenant_id: tenantId, user_id: userId, }, }, }); if (!membership) { throw new Error('用户不是该租户的成员'); } // 删除租户成员关系和模块权限 await prisma.$transaction(async (tx) => { await tx.tenant_members.delete({ where: { id: membership.id }, }); await tx.user_modules.deleteMany({ where: { user_id: userId, tenant_id: tenantId, }, }); }); logger.info('[UserService] Tenant removed from user', { userId, tenantId, removedBy: removerId, }); } /** * 更新用户在指定租户的模块权限 */ export async function updateUserModules( userId: string, data: UpdateUserModulesRequest, updaterId: string ): Promise { // 检查用户是否是该租户成员 const membership = await prisma.tenant_members.findUnique({ where: { tenant_id_user_id: { tenant_id: data.tenantId, user_id: userId, }, }, }); if (!membership) { throw new Error('用户不是该租户的成员'); } // 获取租户订阅的模块 const tenantModules = await prisma.tenant_modules.findMany({ where: { tenant_id: data.tenantId, is_enabled: true }, }); const tenantModuleCodes = tenantModules.map((tm) => tm.module_code); // 验证请求的模块是否在租户订阅范围内 const invalidModules = data.modules.filter((m) => !tenantModuleCodes.includes(m)); if (invalidModules.length > 0) { throw new Error(`以下模块不在租户订阅范围内: ${invalidModules.join(', ')}`); } // 更新用户模块权限 await prisma.$transaction(async (tx) => { // 删除旧的模块权限 await tx.user_modules.deleteMany({ where: { user_id: userId, tenant_id: data.tenantId, }, }); // 创建新的模块权限 if (data.modules.length > 0) { await tx.user_modules.createMany({ data: data.modules.map((moduleCode) => ({ id: uuidv4(), user_id: userId, tenant_id: data.tenantId, module_code: moduleCode, is_enabled: true, })), }); } }); logger.info('[UserService] User modules updated', { userId, tenantId: data.tenantId, modules: data.modules, updatedBy: updaterId, }); } /** * 批量导入用户 */ export async function importUsers( rows: ImportUserRow[], defaultTenantId: string, importerId: string ): Promise { const result: ImportResult = { success: 0, failed: 0, errors: [], }; for (let i = 0; i < rows.length; i++) { const row = rows[i]; const rowNumber = i + 2; // Excel行号(跳过表头) try { // 验证手机号 if (!row.phone || !/^1[3-9]\d{9}$/.test(row.phone)) { throw new Error('手机号格式不正确'); } // 验证姓名 if (!row.name || row.name.trim().length === 0) { throw new Error('姓名不能为空'); } // 解析角色 const role = parseRole(row.role); // 解析租户 let tenantId = defaultTenantId; if (row.tenantCode) { const tenant = await prisma.tenants.findUnique({ where: { code: row.tenantCode }, }); if (!tenant) { throw new Error(`租户代码 ${row.tenantCode} 不存在`); } tenantId = tenant.id; } // 解析科室 let departmentId: string | undefined; if (row.departmentName) { const department = await prisma.departments.findFirst({ where: { name: row.departmentName, tenant_id: tenantId, }, }); if (!department) { throw new Error(`科室 ${row.departmentName} 不存在`); } departmentId = department.id; } // 解析模块 const modules = row.modules ? row.modules.split(',').map((m) => m.trim().toUpperCase()) : undefined; // 创建用户 await createUser( { phone: row.phone, name: row.name.trim(), email: row.email, role, tenantId, departmentId, allowedModules: modules, }, importerId ); result.success++; } catch (error: any) { result.failed++; result.errors.push({ row: rowNumber, phone: row.phone || '', error: error.message, }); } } logger.info('[UserService] Batch import completed', { success: result.success, failed: result.failed, importedBy: importerId, }); return result; } /** * 获取所有租户列表(用于下拉选择) */ export async function getAllTenants() { return prisma.tenants.findMany({ where: { status: 'ACTIVE' }, select: { id: true, code: true, name: true, type: true, }, orderBy: { name: 'asc' }, }); } /** * 获取租户的科室列表(用于下拉选择) */ export async function getDepartmentsByTenant(tenantId: string) { return prisma.departments.findMany({ where: { tenant_id: tenantId }, select: { id: true, name: true, parent_id: true, }, orderBy: { name: 'asc' }, }); } /** * 获取租户的模块列表(用于模块配置) */ export async function getModulesByTenant(tenantId: string) { const tenantModules = await prisma.tenant_modules.findMany({ where: { tenant_id: tenantId, is_enabled: true }, }); const allModules = await prisma.modules.findMany({ where: { is_active: true }, orderBy: { sort_order: 'asc' }, }); return allModules.map((m) => ({ code: m.code, name: m.name, isSubscribed: tenantModules.some((tm) => tm.module_code === m.code), })); } // ============ 辅助函数 ============ function getModuleName(code: string): string { const moduleNames: Record = { AIA: 'AI智能问答', PKB: '个人知识库', ASL: 'AI智能文献', DC: '数据清洗整理', IIT: 'IIT Manager', RVW: '稿件审查', SSA: '智能统计分析', ST: '统计分析工具', }; return moduleNames[code] || code; } function parseRole(roleStr?: string): UserRole { if (!roleStr) return 'USER'; const roleMap: Record = { 超级管理员: 'SUPER_ADMIN', SUPER_ADMIN: 'SUPER_ADMIN', PROMPT工程师: 'PROMPT_ENGINEER', PROMPT_ENGINEER: 'PROMPT_ENGINEER', 医院管理员: 'HOSPITAL_ADMIN', HOSPITAL_ADMIN: 'HOSPITAL_ADMIN', 药企管理员: 'PHARMA_ADMIN', PHARMA_ADMIN: 'PHARMA_ADMIN', 科室主任: 'DEPARTMENT_ADMIN', DEPARTMENT_ADMIN: 'DEPARTMENT_ADMIN', 普通用户: 'USER', USER: 'USER', }; return roleMap[roleStr.trim()] || 'USER'; }