Backend: - Redesign batch user import: add autoInheritModules param, users auto-inherit tenant modules when true - Add module validation: reject modules not subscribed by the tenant - Soften department validation: skip instead of fail when department name not found - Fix RVW skill status semantics: review findings (ERROR issues) no longer mark skill as error - Add parallel execution support to SkillExecutor via parallelGroup - Configure Editorial + Methodology skills to run in parallel (~240s -> ~130s) - Update legacy bridge error message to user-friendly text Frontend: - Redesign ImportUserModal: 4-step flow (select tenant -> upload -> preview -> result) - Simplify import template: remove tenant code and module columns - Show tenant subscribed modules before import with auto-inherit option - Fix isLegacyEmbed modules bypassing RouteGuard and TopNavigation permission checks - Hide ASL fulltext screening (step 3), renumber subsequent nav items - Add ExtractionWorkbenchGuide page when no taskId provided - Update legacy system error message to network-friendly text Docs: - Update deployment changelog with BE-9, FE-11 entries Made-with: Cursor
532 lines
13 KiB
TypeScript
532 lines
13 KiB
TypeScript
/**
|
|
* 用户管理控制器
|
|
* @description 处理用户管理相关的 HTTP 请求
|
|
*/
|
|
|
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
|
import { logger } from '../../../common/logging/index.js';
|
|
import * as userService from '../services/userService.js';
|
|
import {
|
|
ListUsersQuery,
|
|
CreateUserRequest,
|
|
UpdateUserRequest,
|
|
AssignTenantRequest,
|
|
UpdateUserModulesRequest,
|
|
ImportUserRow,
|
|
} from '../types/user.types.js';
|
|
|
|
/**
|
|
* 获取用户列表
|
|
*/
|
|
export async function listUsers(
|
|
request: FastifyRequest<{ Querystring: ListUsersQuery }>,
|
|
reply: FastifyReply
|
|
) {
|
|
try {
|
|
const user = request.user!;
|
|
const scope = await userService.getUserQueryScope(user.role, user.tenantId, user.userId);
|
|
const result = await userService.listUsers(request.query, scope);
|
|
|
|
return reply.send({
|
|
code: 0,
|
|
message: 'success',
|
|
data: result,
|
|
});
|
|
} catch (error: any) {
|
|
logger.error('[UserController] listUsers error:', error);
|
|
return reply.status(500).send({
|
|
code: 500,
|
|
message: error.message || '获取用户列表失败',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取用户详情
|
|
*/
|
|
export async function getUserById(
|
|
request: FastifyRequest<{ Params: { id: string } }>,
|
|
reply: FastifyReply
|
|
) {
|
|
try {
|
|
const user = request.user!;
|
|
const scope = await userService.getUserQueryScope(user.role, user.tenantId, user.userId);
|
|
const result = await userService.getUserById(request.params.id, scope);
|
|
|
|
if (!result) {
|
|
return reply.status(404).send({
|
|
code: 404,
|
|
message: '用户不存在',
|
|
});
|
|
}
|
|
|
|
return reply.send({
|
|
code: 0,
|
|
message: 'success',
|
|
data: result,
|
|
});
|
|
} catch (error: any) {
|
|
logger.error('[UserController] getUserById error:', error);
|
|
return reply.status(500).send({
|
|
code: 500,
|
|
message: error.message || '获取用户详情失败',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 创建用户
|
|
*/
|
|
export async function createUser(
|
|
request: FastifyRequest<{ Body: CreateUserRequest }>,
|
|
reply: FastifyReply
|
|
) {
|
|
try {
|
|
const creator = request.user!;
|
|
|
|
// HOSPITAL_ADMIN 和 PHARMA_ADMIN 只能在自己的租户内创建用户
|
|
if (
|
|
(creator.role === 'HOSPITAL_ADMIN' || creator.role === 'PHARMA_ADMIN') &&
|
|
request.body.tenantId !== creator.tenantId
|
|
) {
|
|
return reply.status(403).send({
|
|
code: 403,
|
|
message: '无权限在其他租户创建用户',
|
|
});
|
|
}
|
|
|
|
const result = await userService.createUser(request.body, creator.userId);
|
|
|
|
return reply.status(201).send({
|
|
code: 0,
|
|
message: '用户创建成功',
|
|
data: result,
|
|
});
|
|
} catch (error: any) {
|
|
logger.error('[UserController] createUser error:', error);
|
|
|
|
if (error.message.includes('已存在')) {
|
|
return reply.status(400).send({
|
|
code: 400,
|
|
message: error.message,
|
|
});
|
|
}
|
|
|
|
return reply.status(500).send({
|
|
code: 500,
|
|
message: error.message || '创建用户失败',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 更新用户
|
|
*/
|
|
export async function updateUser(
|
|
request: FastifyRequest<{ Params: { id: string }; Body: UpdateUserRequest }>,
|
|
reply: FastifyReply
|
|
) {
|
|
try {
|
|
const updater = request.user!;
|
|
const scope = await userService.getUserQueryScope(updater.role, updater.tenantId, updater.userId);
|
|
|
|
const result = await userService.updateUser(
|
|
request.params.id,
|
|
request.body,
|
|
scope,
|
|
updater.userId
|
|
);
|
|
|
|
return reply.send({
|
|
code: 0,
|
|
message: '用户更新成功',
|
|
data: result,
|
|
});
|
|
} catch (error: any) {
|
|
logger.error('[UserController] updateUser error:', error);
|
|
|
|
if (error.message.includes('不存在') || error.message.includes('无权限')) {
|
|
return reply.status(404).send({
|
|
code: 404,
|
|
message: error.message,
|
|
});
|
|
}
|
|
|
|
if (error.message.includes('已存在')) {
|
|
return reply.status(400).send({
|
|
code: 400,
|
|
message: error.message,
|
|
});
|
|
}
|
|
|
|
return reply.status(500).send({
|
|
code: 500,
|
|
message: error.message || '更新用户失败',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 更新用户状态(启用/禁用)
|
|
*/
|
|
export async function updateUserStatus(
|
|
request: FastifyRequest<{
|
|
Params: { id: string };
|
|
Body: { status: 'active' | 'disabled' };
|
|
}>,
|
|
reply: FastifyReply
|
|
) {
|
|
try {
|
|
const updater = request.user!;
|
|
const scope = await userService.getUserQueryScope(updater.role, updater.tenantId, updater.userId);
|
|
|
|
await userService.updateUserStatus(
|
|
request.params.id,
|
|
request.body.status,
|
|
scope,
|
|
updater.userId
|
|
);
|
|
|
|
return reply.send({
|
|
code: 0,
|
|
message: request.body.status === 'active' ? '用户已启用' : '用户已禁用',
|
|
});
|
|
} catch (error: any) {
|
|
logger.error('[UserController] updateUserStatus error:', error);
|
|
|
|
if (error.message.includes('不存在') || error.message.includes('无权限')) {
|
|
return reply.status(404).send({
|
|
code: 404,
|
|
message: error.message,
|
|
});
|
|
}
|
|
|
|
return reply.status(500).send({
|
|
code: 500,
|
|
message: error.message || '更新状态失败',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 重置用户密码
|
|
*/
|
|
export async function resetUserPassword(
|
|
request: FastifyRequest<{ Params: { id: string } }>,
|
|
reply: FastifyReply
|
|
) {
|
|
try {
|
|
const resetter = request.user!;
|
|
const scope = await userService.getUserQueryScope(resetter.role, resetter.tenantId, resetter.userId);
|
|
|
|
await userService.resetUserPassword(request.params.id, scope, resetter.userId);
|
|
|
|
return reply.send({
|
|
code: 0,
|
|
message: '密码已重置为默认密码',
|
|
});
|
|
} catch (error: any) {
|
|
logger.error('[UserController] resetUserPassword error:', error);
|
|
|
|
if (error.message.includes('不存在') || error.message.includes('无权限')) {
|
|
return reply.status(404).send({
|
|
code: 404,
|
|
message: error.message,
|
|
});
|
|
}
|
|
|
|
return reply.status(500).send({
|
|
code: 500,
|
|
message: error.message || '重置密码失败',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 分配租户给用户
|
|
*/
|
|
export async function assignTenantToUser(
|
|
request: FastifyRequest<{
|
|
Params: { id: string };
|
|
Body: AssignTenantRequest;
|
|
}>,
|
|
reply: FastifyReply
|
|
) {
|
|
try {
|
|
const assigner = request.user!;
|
|
|
|
// 只有 SUPER_ADMIN 可以分配租户
|
|
if (assigner.role !== 'SUPER_ADMIN') {
|
|
return reply.status(403).send({
|
|
code: 403,
|
|
message: '无权限分配租户',
|
|
});
|
|
}
|
|
|
|
await userService.assignTenantToUser(request.params.id, request.body, assigner.userId);
|
|
|
|
return reply.send({
|
|
code: 0,
|
|
message: '租户分配成功',
|
|
});
|
|
} catch (error: any) {
|
|
logger.error('[UserController] assignTenantToUser error:', error);
|
|
|
|
if (error.message.includes('不存在')) {
|
|
return reply.status(404).send({
|
|
code: 404,
|
|
message: error.message,
|
|
});
|
|
}
|
|
|
|
if (error.message.includes('已是')) {
|
|
return reply.status(400).send({
|
|
code: 400,
|
|
message: error.message,
|
|
});
|
|
}
|
|
|
|
return reply.status(500).send({
|
|
code: 500,
|
|
message: error.message || '分配租户失败',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 从租户移除用户
|
|
*/
|
|
export async function removeTenantFromUser(
|
|
request: FastifyRequest<{
|
|
Params: { id: string; tenantId: string };
|
|
}>,
|
|
reply: FastifyReply
|
|
) {
|
|
try {
|
|
const remover = request.user!;
|
|
|
|
// 只有 SUPER_ADMIN 可以移除租户
|
|
if (remover.role !== 'SUPER_ADMIN') {
|
|
return reply.status(403).send({
|
|
code: 403,
|
|
message: '无权限移除租户',
|
|
});
|
|
}
|
|
|
|
await userService.removeTenantFromUser(
|
|
request.params.id,
|
|
request.params.tenantId,
|
|
remover.userId
|
|
);
|
|
|
|
return reply.send({
|
|
code: 0,
|
|
message: '租户移除成功',
|
|
});
|
|
} catch (error: any) {
|
|
logger.error('[UserController] removeTenantFromUser error:', error);
|
|
|
|
if (error.message.includes('不存在') || error.message.includes('不是')) {
|
|
return reply.status(404).send({
|
|
code: 404,
|
|
message: error.message,
|
|
});
|
|
}
|
|
|
|
if (error.message.includes('不能移除')) {
|
|
return reply.status(400).send({
|
|
code: 400,
|
|
message: error.message,
|
|
});
|
|
}
|
|
|
|
return reply.status(500).send({
|
|
code: 500,
|
|
message: error.message || '移除租户失败',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 更新用户在指定租户的模块权限
|
|
*/
|
|
export async function updateUserModules(
|
|
request: FastifyRequest<{
|
|
Params: { id: string };
|
|
Body: UpdateUserModulesRequest;
|
|
}>,
|
|
reply: FastifyReply
|
|
) {
|
|
try {
|
|
const updater = request.user!;
|
|
|
|
// SUPER_ADMIN 可以操作任意租户
|
|
// HOSPITAL_ADMIN/PHARMA_ADMIN 只能操作自己租户的用户
|
|
if (
|
|
updater.role !== 'SUPER_ADMIN' &&
|
|
request.body.tenantId !== updater.tenantId
|
|
) {
|
|
return reply.status(403).send({
|
|
code: 403,
|
|
message: '无权限修改其他租户的用户模块权限',
|
|
});
|
|
}
|
|
|
|
await userService.updateUserModules(request.params.id, request.body, updater.userId);
|
|
|
|
return reply.send({
|
|
code: 0,
|
|
message: '模块权限更新成功',
|
|
});
|
|
} catch (error: any) {
|
|
logger.error('[UserController] updateUserModules error:', error);
|
|
|
|
if (error.message.includes('不是')) {
|
|
return reply.status(404).send({
|
|
code: 404,
|
|
message: error.message,
|
|
});
|
|
}
|
|
|
|
if (error.message.includes('不在')) {
|
|
return reply.status(400).send({
|
|
code: 400,
|
|
message: error.message,
|
|
});
|
|
}
|
|
|
|
return reply.status(500).send({
|
|
code: 500,
|
|
message: error.message || '更新模块权限失败',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 批量导入用户
|
|
*/
|
|
export async function importUsers(
|
|
request: FastifyRequest<{
|
|
Body: {
|
|
users: ImportUserRow[];
|
|
defaultTenantId?: string;
|
|
autoInheritModules?: boolean;
|
|
};
|
|
}>,
|
|
reply: FastifyReply
|
|
) {
|
|
try {
|
|
const importer = request.user!;
|
|
const autoInheritModules = request.body.autoInheritModules ?? true;
|
|
|
|
// 确定默认租户
|
|
let defaultTenantId = request.body.defaultTenantId;
|
|
if (!defaultTenantId) {
|
|
if (importer.role === 'SUPER_ADMIN') {
|
|
return reply.status(400).send({
|
|
code: 400,
|
|
message: '请指定默认租户',
|
|
});
|
|
}
|
|
defaultTenantId = importer.tenantId;
|
|
}
|
|
|
|
// HOSPITAL_ADMIN/PHARMA_ADMIN 只能导入到自己的租户
|
|
if (
|
|
(importer.role === 'HOSPITAL_ADMIN' || importer.role === 'PHARMA_ADMIN') &&
|
|
defaultTenantId !== importer.tenantId
|
|
) {
|
|
return reply.status(403).send({
|
|
code: 403,
|
|
message: '无权限导入到其他租户',
|
|
});
|
|
}
|
|
|
|
const result = await userService.importUsers(
|
|
request.body.users,
|
|
defaultTenantId,
|
|
importer.userId,
|
|
autoInheritModules
|
|
);
|
|
|
|
return reply.send({
|
|
code: 0,
|
|
message: `导入完成:成功 ${result.success} 条,失败 ${result.failed} 条`,
|
|
data: result,
|
|
});
|
|
} catch (error: any) {
|
|
logger.error('[UserController] importUsers error:', error);
|
|
return reply.status(500).send({
|
|
code: 500,
|
|
message: error.message || '批量导入失败',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取所有租户列表(用于下拉选择)
|
|
*/
|
|
export async function getAllTenants(request: FastifyRequest, reply: FastifyReply) {
|
|
try {
|
|
const result = await userService.getAllTenants();
|
|
return reply.send({
|
|
code: 0,
|
|
message: 'success',
|
|
data: result,
|
|
});
|
|
} catch (error: any) {
|
|
logger.error('[UserController] getAllTenants error:', error);
|
|
return reply.status(500).send({
|
|
code: 500,
|
|
message: error.message || '获取租户列表失败',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取租户的科室列表(用于下拉选择)
|
|
*/
|
|
export async function getDepartmentsByTenant(
|
|
request: FastifyRequest<{ Params: { tenantId: string } }>,
|
|
reply: FastifyReply
|
|
) {
|
|
try {
|
|
const result = await userService.getDepartmentsByTenant(request.params.tenantId);
|
|
return reply.send({
|
|
code: 0,
|
|
message: 'success',
|
|
data: result,
|
|
});
|
|
} catch (error: any) {
|
|
logger.error('[UserController] getDepartmentsByTenant error:', error);
|
|
return reply.status(500).send({
|
|
code: 500,
|
|
message: error.message || '获取科室列表失败',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取租户的模块列表(用于模块配置)
|
|
*/
|
|
export async function getModulesByTenant(
|
|
request: FastifyRequest<{ Params: { tenantId: string } }>,
|
|
reply: FastifyReply
|
|
) {
|
|
try {
|
|
const result = await userService.getModulesByTenant(request.params.tenantId);
|
|
return reply.send({
|
|
code: 0,
|
|
message: 'success',
|
|
data: result,
|
|
});
|
|
} catch (error: any) {
|
|
logger.error('[UserController] getModulesByTenant error:', error);
|
|
return reply.status(500).send({
|
|
code: 500,
|
|
message: error.message || '获取模块列表失败',
|
|
});
|
|
}
|
|
}
|
|
|