Features - User Management (Phase 4.1): - Database: Add user_modules table for fine-grained module permissions - Database: Add 4 user permissions (view/create/edit/delete) to role_permissions - Backend: UserService (780 lines) - CRUD with tenant isolation - Backend: UserController + UserRoutes (648 lines) - 13 API endpoints - Backend: Batch import users from Excel - Frontend: UserListPage (412 lines) - list/filter/search/pagination - Frontend: UserFormPage (341 lines) - create/edit with module config - Frontend: UserDetailPage (393 lines) - details/tenant/module management - Frontend: 3 modal components (592 lines) - import/assign/configure - API: GET/POST/PUT/DELETE /api/admin/users/* endpoints Architecture Upgrade - Module Permission System: - Backend: Add getUserModules() method in auth.service - Backend: Login API returns modules array in user object - Frontend: AuthContext adds hasModule() method - Frontend: Navigation filters modules based on user.modules - Frontend: RouteGuard checks requiredModule instead of requiredVersion - Frontend: Remove deprecated version-based permission system - UX: Only show accessible modules in navigation (clean UI) - UX: Smart redirect after login (avoid 403 for regular users) Fixes: - Fix UTF-8 encoding corruption in ~100 docs files - Fix pageSize type conversion in userService (String to Number) - Fix authUser undefined error in TopNavigation - Fix login redirect logic with role-based access check - Update Git commit guidelines v1.2 with UTF-8 safety rules Database Changes: - CREATE TABLE user_modules (user_id, tenant_id, module_code, is_enabled) - ADD UNIQUE CONSTRAINT (user_id, tenant_id, module_code) - INSERT 4 permissions + role assignments - UPDATE PUBLIC tenant with 8 module subscriptions Technical: - Backend: 5 new files (~2400 lines) - Frontend: 10 new files (~2500 lines) - Docs: 1 development record + 2 status updates + 1 guideline update - Total: ~4900 lines of code Status: User management 100% complete, module permission system operational
901 lines
21 KiB
TypeScript
901 lines
21 KiB
TypeScript
/**
|
||
* 用户管理服务
|
||
* @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<string | undefined> {
|
||
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<UserQueryScope> {
|
||
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<PaginatedResponse<UserListItem>> {
|
||
// 确保 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<UserDetail | null> {
|
||
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<UserDetail> {
|
||
// 检查手机号是否已存在
|
||
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<UserDetail> {
|
||
// 检查用户是否存在且在权限范围内
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
// 检查用户是否存在
|
||
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<void> {
|
||
// 检查用户是否存在
|
||
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<void> {
|
||
// 检查用户是否是该租户成员
|
||
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<ImportResult> {
|
||
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<string, string> = {
|
||
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<string, UserRole> = {
|
||
超级管理员: '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';
|
||
}
|
||
|